# MCP Server
Source: https://docs.pictify.io/agent-integration/mcp-server
Integrate Pictify with AI agents using Model Context Protocol
# MCP Server
Pictify provides an MCP (Model Context Protocol) server that enables AI agents to generate images, GIFs, and PDFs programmatically.
## What is MCP?
Model Context Protocol is a standard for connecting AI models to external tools and data sources. With Pictify's MCP server, AI assistants like Claude can:
* Generate images from descriptions
* Create social media graphics
* Render templates with dynamic data
* Capture screenshots of web pages
## Installation
### Claude Desktop
Add Pictify to your Claude Desktop configuration:
```json theme={null}
// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
// %APPDATA%\Claude\claude_desktop_config.json (Windows)
{
"mcpServers": {
"pictify": {
"command": "npx",
"args": ["-y", "@pictify/mcp-server"],
"env": {
"PICTIFY_API_KEY": "YOUR_API_KEY"
}
}
}
}
```
Restart Claude Desktop after adding the configuration.
### Claude Code
```bash theme={null}
claude mcp add pictify -- npx -y @pictify/mcp-server
```
Set your API key:
```bash theme={null}
export PICTIFY_API_KEY=YOUR_API_KEY
```
### Other MCP Clients
The Pictify MCP server follows the standard MCP protocol and works with any compatible client.
## Available Tools
The server ships 26 tools. The most-used ones are detailed below; the full set:
| Group | Tools |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Images | `pictify_create_image` (HTML, URL screenshot, or template), `pictify_create_canvas_image`, `pictify_get_image`, `pictify_list_images` |
| GIFs | `pictify_create_gif`, `pictify_capture_gif`, `pictify_get_gif`, `pictify_list_gifs` |
| PDFs | `pictify_render_pdf`, `pictify_render_multi_page_pdf`, `pictify_list_pdf_presets` |
| Templates | `pictify_create_template`, `pictify_get_template`, `pictify_update_template`, `pictify_delete_template`, `pictify_list_templates`, `pictify_get_template_variables`, `pictify_render_template` |
| Batch | `pictify_batch_render` (inline rows or CSV mode), `pictify_get_batch_results`, `pictify_cancel_batch` |
| Video | `pictify_list_video_templates`, `pictify_get_video_template_variables`, `pictify_render_video` (MP4 or GIF), `pictify_generate_video_template` (AI-designed), `pictify_create_video_template` (agent-authored Remotion scene, compile-gated) |
### `pictify_create_image`
Generate a static image from HTML, a URL screenshot, or a template. Provide exactly one of `html`, `url`, or `template`.
**Parameters:**
| Name | Type | Required | Description |
| --------------- | ------ | -------- | ------------------------------------------- |
| `html` | string | One of | HTML/CSS content to render |
| `url` | string | One of | Public URL to screenshot |
| `template` | string | One of | Template UID (use with `variables`) |
| `variables` | object | No | Template variables (when `template` is set) |
| `width` | number | No | Width in pixels, 1–4000 (default: 1200) |
| `height` | number | No | Height in pixels, 1–4000 (default: 630) |
| `selector` | string | No | CSS selector to capture a single element |
| `fileExtension` | string | No | `png` (default), `jpg`, `jpeg`, or `webp` |
**Example:**
```
Create a social media card with the title "Hello World" on a purple gradient background, 1200x630 pixels.
```
The AI will call `pictify_create_image` with appropriate HTML. To screenshot a page, it passes `url` instead (optionally with `selector` to crop to a section).
### `pictify_render_template`
Render a saved template with variables.
**Parameters:**
| Name | Type | Required | Description |
| ------------ | --------- | -------- | ----------------------------------------------------- |
| `templateId` | string | Yes | Template UID |
| `variables` | object | No | Template variables |
| `format` | string | No | `png` (default), `jpeg`, `webp`, or `pdf` |
| `quality` | number | No | Output quality 0.1–1.0 (JPEG/WebP only, default: 0.9) |
| `layout` | string | No | Render a specific layout variant |
| `layouts` | string\[] | No | Render multiple layout variants in one call |
**Example:**
```
Render my blog post template with title "AI in 2026" and author "Jane Smith".
```
### `pictify_create_gif`
Create an animated GIF from animated HTML, a URL, or a template. Provide exactly one of `html`, `url`, or `template`.
**Parameters:**
| Name | Type | Required | Description |
| ----------- | ------ | -------- | ------------------------------------------- |
| `html` | string | One of | HTML with CSS `@keyframes`/transitions |
| `url` | string | One of | URL to capture as a GIF |
| `template` | string | One of | Template UID (use with `variables`) |
| `variables` | object | No | Template variables (when `template` is set) |
| `width` | number | No | Width in pixels, 1–2000 (default: 800) |
| `height` | number | No | Height in pixels, 1–2000 (default: 600) |
To record a live web page over a fixed duration, use the `pictify_capture_gif` tool instead — it takes a `url` plus `frameDurationSeconds` (1–30) and a `quality` preset (`low` | `medium` | `high`).
### `pictify_list_templates`
List available templates.
**Parameters:**
| Name | Type | Required | Description |
| -------------- | ------ | -------- | -------------------------------------------- |
| `page` | number | No | Page number, starts at 1 (default: 1) |
| `limit` | number | No | Templates per page, 1–100 (default: 12) |
| `sort` | string | No | `newest` (default), `oldest`, or `name` |
| `outputFormat` | string | No | Filter by `all` (default), `image`, or `pdf` |
## Example Conversations
### Creating Social Graphics
**User:** Create a Twitter header image for my AI startup "NeuralFlow" with a dark theme and neural network visualization.
**Claude:** I'll create a Twitter header image for NeuralFlow with a dark neural network theme.
*Calls `pictify_create_image` with HTML for the header*
Here's your Twitter header image: \[Image URL]
The image features:
* Deep purple gradient background
* Animated neural network pattern
* "NeuralFlow" in modern typography
* Optimal 1500x500 dimensions for Twitter
### Rendering Templates
**User:** I have a blog post template. Generate images for these three posts:
1. "The Future of AI" by John Doe
2. "Web3 Explained" by Jane Smith
3. "Cloud Computing 101" by Bob Wilson
**Claude:** I'll render your blog post template for each article.
*Calls `pictify_render_template` three times with different variables*
Here are your blog post images:
1. "The Future of AI" - \[Image URL]
2. "Web3 Explained" - \[Image URL]
3. "Cloud Computing 101" - \[Image URL]
### Screenshots for Documentation
**User:** Take screenshots of our app's dashboard, settings page, and billing page for documentation.
**Claude:** I'll capture screenshots of each page for your documentation.
*Calls `pictify_create_image` with a `url` for each page*
Here are your documentation screenshots:
1. Dashboard - \[Image URL]
2. Settings - \[Image URL]
3. Billing - \[Image URL]
## Configuration Options
### Environment Variables
| Variable | Description | Default |
| ------------------ | -------------------- | ------------------------ |
| `PICTIFY_API_KEY` | Your Pictify API key | Required |
| `PICTIFY_BASE_URL` | Custom API base URL | `https://api.pictify.io` |
### Server Options
```json theme={null}
{
"mcpServers": {
"pictify": {
"command": "npx",
"args": ["-y", "@pictify/mcp-server"],
"env": {
"PICTIFY_API_KEY": "your-api-key",
"PICTIFY_BASE_URL": "https://api.pictify.io"
}
}
}
}
```
## Building Custom Agents
### With Anthropic's Claude API
```typescript theme={null}
import Anthropic from '@anthropic-ai/sdk';
import { Pictify } from '@pictify/sdk';
const anthropic = new Anthropic();
const pictify = new Pictify({ apiKey: process.env.PICTIFY_API_KEY! });
const tools = [
{
name: 'create_image',
description: 'Generate an image from HTML content',
input_schema: {
type: 'object',
properties: {
html: { type: 'string', description: 'HTML content to render' },
width: { type: 'number', description: 'Image width in pixels' },
height: { type: 'number', description: 'Image height in pixels' }
},
required: ['html', 'width', 'height']
}
}
];
async function handleToolCall(name: string, input: any) {
if (name === 'create_image') {
const image = await pictify.renderHtml(input);
return { url: image.url };
}
}
async function chat(userMessage: string) {
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
tools,
messages: [{ role: 'user', content: userMessage }]
});
// Handle tool calls
for (const block of response.content) {
if (block.type === 'tool_use') {
const result = await handleToolCall(block.name, block.input);
// Continue conversation with tool result...
}
}
}
```
### With LangChain
```python theme={null}
from langchain.tools import Tool
from langchain.agents import initialize_agent
from pictify import Pictify
pictify = Pictify(api_key='your-api-key')
def create_image(html: str, width: int = 1200, height: int = 630) -> str:
"""Generate an image from HTML content."""
result = pictify.render_html(html=html, width=width, height=height)
return result.url
tools = [
Tool(
name="create_image",
func=create_image,
description="Generate an image from HTML. Input should be HTML string."
)
]
agent = initialize_agent(tools, llm, agent="zero-shot-react-description")
```
## Best Practices
### 1. Provide Clear Descriptions
Give templates and tools clear descriptions so the AI understands when to use them:
```typescript theme={null}
{
name: 'render_blog_card',
description: 'Render a blog post social card. Use for creating Open Graph images for blog posts. Requires title, description, and author name.'
}
```
### 2. Handle Errors Gracefully
```typescript theme={null}
async function handleToolCall(name: string, input: any) {
try {
return await executeToolCall(name, input);
} catch (error) {
return {
error: true,
message: `Failed to ${name}: ${error.message}`
};
}
}
```
### 3. Validate Input
```typescript theme={null}
if (!input.html || !input.width || !input.height) {
return { error: true, message: 'Missing required parameters' };
}
if (input.width > 4000 || input.height > 4000) {
return { error: true, message: 'Dimensions exceed maximum (4000x4000)' };
}
```
### 4. Cache Results
For repeated requests, cache the results:
```typescript theme={null}
const cache = new Map();
async function createImage(options: RenderHtmlOptions) {
const cacheKey = JSON.stringify(options);
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const result = await pictify.renderHtml(options);
cache.set(cacheKey, result);
return result;
}
```
# Tool Schemas
Source: https://docs.pictify.io/agent-integration/tool-schemas
JSON schemas for AI tool integration
# Tool Schemas
Use these JSON schemas to integrate Pictify with AI agents, function calling, and tool-use systems.
## Image Generation
### Create Image
```json theme={null}
{
"name": "pictify_create_image",
"description": "Generate an image from HTML content. Supports PNG, JPEG, and WebP formats. Use for creating social media graphics, banners, cards, and any visual content from HTML/CSS.",
"input_schema": {
"type": "object",
"properties": {
"html": {
"type": "string",
"description": "HTML content to render. Can include inline CSS and external fonts."
},
"width": {
"type": "integer",
"description": "Output width in pixels (1-4000)",
"minimum": 1,
"maximum": 4000
},
"height": {
"type": "integer",
"description": "Output height in pixels (1-4000)",
"minimum": 1,
"maximum": 4000
},
"format": {
"type": "string",
"enum": ["png", "jpeg", "webp"],
"default": "png",
"description": "Output image format"
},
"quality": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 85,
"description": "Quality for JPEG/WebP (1-100)"
},
"transparent": {
"type": "boolean",
"default": false,
"description": "Enable transparent background (PNG/WebP only)"
}
},
"required": ["html", "width", "height"]
}
}
```
### Screenshot a URL
There is no separate screenshot tool — screenshots go through `pictify_create_image` with `url` instead of `html` (the two are mutually exclusive, along with `template`):
```json theme={null}
{
"name": "pictify_create_image",
"arguments": {
"url": "https://example.com/pricing",
"width": 1280,
"height": 800
}
}
```
The page must be publicly accessible over http(s).
## Template Operations
### Render Template
```json theme={null}
{
"name": "pictify_render_template",
"description": "Render a saved template with dynamic variables. Use for generating personalized images from pre-designed templates.",
"input_schema": {
"type": "object",
"properties": {
"templateId": {
"type": "string",
"description": "Template ID (e.g., tmpl_abc123)"
},
"variables": {
"type": "object",
"description": "Variables to inject into the template",
"additionalProperties": true
},
"format": {
"type": "string",
"enum": ["png", "jpeg", "webp"],
"default": "png"
}
},
"required": ["templateId"]
}
}
```
### List Templates
```json theme={null}
{
"name": "pictify_list_templates",
"description": "List available templates. Use to discover what templates are available before rendering.",
"input_schema": {
"type": "object",
"properties": {
"limit": {
"type": "integer",
"default": 20,
"maximum": 100,
"description": "Maximum number of templates to return"
},
"page": {
"type": "integer",
"default": 1,
"description": "Page number for pagination"
}
}
}
}
```
### Get Template Variables
```json theme={null}
{
"name": "pictify_get_template_variables",
"description": "Get the list of variables required by a template. Use before rendering to understand what data is needed.",
"input_schema": {
"type": "object",
"properties": {
"templateId": {
"type": "string",
"description": "Template ID to get variables for"
}
},
"required": ["templateId"]
}
}
```
## GIF Generation
### Create GIF
```json theme={null}
{
"name": "pictify_create_gif",
"description": "Create an animated GIF from HTML with CSS animations. Use for loading spinners, animated banners, or any looping animation.",
"input_schema": {
"type": "object",
"properties": {
"html": {
"type": "string",
"description": "HTML content with CSS animations"
},
"width": {
"type": "integer",
"description": "GIF width in pixels (max 1200)",
"maximum": 1200
},
"height": {
"type": "integer",
"description": "GIF height in pixels (max 1200)",
"maximum": 1200
},
"quality": {
"type": "string",
"enum": ["low", "medium", "high"],
"default": "medium",
"description": "Quality preset affecting frame rate and duration"
}
},
"required": ["html", "width", "height"]
}
}
```
### Capture GIF from URL
```json theme={null}
{
"name": "pictify_capture_gif",
"description": "Record a GIF from a live web page. Use for capturing animations or interactions on existing websites.",
"input_schema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "URL to capture"
},
"width": {
"type": "integer",
"default": 800,
"description": "Capture width"
},
"height": {
"type": "integer",
"default": 600,
"description": "Capture height"
},
"duration": {
"type": "number",
"default": 5,
"description": "Recording duration in seconds"
},
"quality": {
"type": "string",
"enum": ["low", "medium", "high"],
"default": "medium"
}
},
"required": ["url"]
}
}
```
## PDF Generation
### Render PDF
```json theme={null}
{
"name": "pictify_render_pdf",
"description": "Generate a PDF from a template. Use for invoices, reports, certificates, and other documents.",
"input_schema": {
"type": "object",
"properties": {
"templateId": {
"type": "string",
"description": "Template ID to render"
},
"variables": {
"type": "object",
"description": "Variables for the template"
},
"preset": {
"type": "string",
"enum": ["A4", "Letter", "Legal", "A3", "A5", "custom"],
"default": "A4",
"description": "Paper size preset"
},
"landscape": {
"type": "boolean",
"default": false,
"description": "Use landscape orientation"
},
"margins": {
"type": "object",
"properties": {
"top": { "type": "number" },
"bottom": { "type": "number" },
"left": { "type": "number" },
"right": { "type": "number" }
},
"description": "Page margins in pixels"
}
},
"required": ["templateId"]
}
}
```
## OpenAI Function Calling Format
For OpenAI's function calling, use this format:
```json theme={null}
{
"type": "function",
"function": {
"name": "pictify_create_image",
"description": "Generate an image from HTML content",
"parameters": {
"type": "object",
"properties": {
"html": {
"type": "string",
"description": "HTML content to render"
},
"width": {
"type": "integer",
"description": "Width in pixels"
},
"height": {
"type": "integer",
"description": "Height in pixels"
}
},
"required": ["html", "width", "height"]
}
}
}
```
## Anthropic Tool Use Format
For Claude's tool use:
```json theme={null}
{
"name": "pictify_create_image",
"description": "Generate an image from HTML content. Returns a URL to the generated image.",
"input_schema": {
"type": "object",
"properties": {
"html": {
"type": "string",
"description": "HTML content to render"
},
"width": {
"type": "integer",
"description": "Width in pixels (1-4000)"
},
"height": {
"type": "integer",
"description": "Height in pixels (1-4000)"
}
},
"required": ["html", "width", "height"]
}
}
```
## Response Schemas
### Image Response
```json theme={null}
{
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "CDN URL of the generated image"
},
"id": {
"type": "string",
"description": "Unique image identifier"
},
"width": {
"type": "integer",
"description": "Actual image width"
},
"height": {
"type": "integer",
"description": "Actual image height"
},
"format": {
"type": "string",
"description": "Image format"
},
"size": {
"type": "integer",
"description": "File size in bytes"
}
}
}
```
### Error Response
```json theme={null}
{
"type": "object",
"properties": {
"type": {
"type": "string",
"format": "uri",
"description": "Error type URI"
},
"title": {
"type": "string",
"description": "Error title"
},
"status": {
"type": "integer",
"description": "HTTP status code"
},
"detail": {
"type": "string",
"description": "Detailed error message"
}
}
}
```
## Usage Examples
### OpenAI
```python theme={null}
import openai
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Create a social card for my blog post about AI"}],
tools=[{
"type": "function",
"function": {
"name": "pictify_create_image",
"description": "Generate an image from HTML",
"parameters": {
"type": "object",
"properties": {
"html": {"type": "string"},
"width": {"type": "integer"},
"height": {"type": "integer"}
},
"required": ["html", "width", "height"]
}
}
}]
)
```
### Anthropic
```python theme={null}
import anthropic
response = anthropic.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[{
"name": "pictify_create_image",
"description": "Generate an image from HTML",
"input_schema": {
"type": "object",
"properties": {
"html": {"type": "string"},
"width": {"type": "integer"},
"height": {"type": "integer"}
},
"required": ["html", "width", "height"]
}
}],
messages=[{"role": "user", "content": "Create a social card for my blog post"}]
)
```
# Batch Operations
Source: https://docs.pictify.io/api-reference/batch
post /templates/{uid}/batch-render
Generate multiple images efficiently with batch processing
# Batch Operations
Generate multiple images from a single template with different variables. Ideal for bulk generation of social cards, certificates, or personalized content.
## Endpoints
| Method | Endpoint | Description |
| ------ | ------------------------------------ | -------------------------------------------------------- |
| `POST` | `/templates/{uid}/batch-render` | [Start batch job](/api-reference/endpoints/batch/render) |
| `GET` | `/templates/batch/{batchId}/results` | [Get results](/api-reference/endpoints/batch/results) |
| `POST` | `/templates/batch/{batchId}/cancel` | [Cancel batch](/api-reference/endpoints/batch/cancel) |
## Two input modes
The same endpoint accepts rows either inline or from a spreadsheet:
**Rows mode** — send `variableSets`, an array of variable objects, one per render:
```json theme={null}
{
"variableSets": [
{ "name": "Maya Chen", "course": "Advanced TypeScript" },
{ "name": "Sam Ortiz", "course": "Advanced TypeScript" }
],
"format": "png"
}
```
**CSV mode** — point at a hosted CSV and map its columns onto template variables. Every row becomes a render, so a spreadsheet of recipients becomes a batch without writing the rows out:
```json theme={null}
{
"csvUrl": "https://example.com/recipients.csv",
"mappings": {
"name": "attendee_name",
"course": "session"
},
"format": "png"
}
```
`mappings` is `{ "templateVariable": "CSV Column" }` — the key is the template variable, the value is the CSV column that fills it. The CSV must be publicly fetchable — the server downloads it once when the batch starts.
## Batch Status
| Status | Description |
| ------------ | -------------------------------- |
| `pending` | Job created, waiting to start |
| `processing` | Currently rendering images |
| `completed` | All items processed successfully |
| `partial` | Completed with some failures |
| `failed` | All items failed |
| `cancelled` | Job was cancelled |
## Webhook Notification
When a batch job completes, Pictify sends a `batch.completed` event to your webhook URL (if provided):
```json theme={null}
{
"event": "batch.completed",
"data": {
"batchId": "batch_xyz789",
"status": "completed",
"totalCount": 100,
"completedCount": 100,
"failedCount": 0
}
}
```
## Limits
| Plan | Max Items per Batch | Concurrent Batches |
| ---------- | ------------------- | ------------------ |
| Free | 10 | 1 |
| Pro | 100 | 3 |
| Business | 1,000 | 10 |
| Enterprise | 10,000 | Unlimited |
## Best Practices
1. **Use webhooks** -- don't poll for results; use webhooks for notification
2. **Handle failures gracefully** -- some items may fail; implement retry logic
3. **Batch appropriately** -- group related renders; avoid tiny batches
4. **Monitor progress** -- track batch status for long-running jobs
5. **Clean up** -- results are stored for 7 days; download and store permanently if needed
# Bindings
Source: https://docs.pictify.io/api-reference/bindings
get /bindings
Connect templates to external data sources for automatic rendering
# Bindings
Bindings connect templates to external data sources, enabling automatic image generation when data changes. Perfect for dynamic dashboards, real-time stats, and automated social media images.
For a step-by-step tutorial, see [Webhook Integration](/guides/webhook-integration).
## How Bindings Work
```
[Your Data API] → [Binding] → [Template] → [Updated Image]
```
1. **Create a binding** -- link a template to an external data URL
2. **Pictify fetches data** -- based on your refresh policy
3. **Template renders** -- variables from the data are injected into the template
4. **Image updates** -- the rendered image URL stays the same, content updates automatically
## Endpoints
| Method | Endpoint | Description |
| -------- | ----------------- | ---------------------------------------------------------- |
| `GET` | `/bindings` | [List bindings](/api-reference/endpoints/bindings/list) |
| `POST` | `/bindings` | [Create binding](/api-reference/endpoints/bindings/create) |
| `GET` | `/bindings/{uid}` | [Get binding](/api-reference/endpoints/bindings/get) |
| `PUT` | `/bindings/{uid}` | [Update binding](/api-reference/endpoints/bindings/update) |
| `DELETE` | `/bindings/{uid}` | [Delete binding](/api-reference/endpoints/bindings/delete) |
## Binding Status
| Status | Description |
| -------- | -------------------------------- |
| `active` | Binding is active and refreshing |
| `paused` | Temporarily disabled |
| `error` | Failed to fetch data or render |
## Refresh Policy Types
| Type | Description |
| --------- | --------------------------------------------------------------- |
| `ttl` | Refresh after time-to-live expires (default, 60-604800 seconds) |
| `etag` | Refresh when ETag changes |
| `webhook` | Refresh when webhook is triggered |
| `manual` | Only refresh when manually triggered |
## Data Mapping
Map fields from your API response to template variables using dot notation and array access:
```json theme={null}
{
"mapping": {
"title": "name",
"stars": "stargazers_count",
"userName": "user.name",
"firstItem": "items[0].name"
}
}
```
## Data Source Authentication
| Type | Description |
| --------------- | ------------------------------------ |
| `api_key` | API key authentication |
| `bearer_token` | Bearer token in Authorization header |
| `basic_auth` | Username/password authentication |
| `custom_header` | Custom header name and value |
## Binding Events
Subscribe to binding events via [Webhooks](/api-reference/webhooks):
| Event | Description |
| ----------------- | ------------------------------ |
| `binding.updated` | Binding successfully refreshed |
| `binding.failed` | Binding refresh failed |
## Best Practices
1. **Set appropriate TTL** -- don't refresh more often than data changes
2. **Use defaults** -- provide fallback values for missing fields
3. **Handle errors gracefully** -- use `serve_stale` for critical images
4. **Monitor binding health** -- set up webhooks for `binding.failed` events
5. **Test before deploying** -- validate your data source and mapping first
# Cancel Batch
Source: https://docs.pictify.io/api-reference/endpoints/batch/cancel
post /templates/batch/{batchId}/cancel
# Batch Render
Source: https://docs.pictify.io/api-reference/endpoints/batch/render
post /templates/{uid}/batch-render
Generate multiple images from a template in one request
# Get Batch Results
Source: https://docs.pictify.io/api-reference/endpoints/batch/results
get /templates/batch/{batchId}/results
# Create Binding
Source: https://docs.pictify.io/api-reference/endpoints/bindings/create
post /bindings
# Delete Binding
Source: https://docs.pictify.io/api-reference/endpoints/bindings/delete
delete /bindings/{uid}
# Get Binding
Source: https://docs.pictify.io/api-reference/endpoints/bindings/get
get /bindings/{uid}
# List Bindings
Source: https://docs.pictify.io/api-reference/endpoints/bindings/list
get /bindings
# Update Binding
Source: https://docs.pictify.io/api-reference/endpoints/bindings/update
put /bindings/{uid}
# Capture GIF from URL
Source: https://docs.pictify.io/api-reference/endpoints/gifs/capture
post /gif/capture
Record a GIF from a live webpage
# Create GIF
Source: https://docs.pictify.io/api-reference/endpoints/gifs/create
post /gif
Generate an animated GIF from HTML with CSS animations
# Get GIF
Source: https://docs.pictify.io/api-reference/endpoints/gifs/get
get /gif/{uid}
Retrieve details of a specific GIF by its UID. No authentication required.
# List GIFs
Source: https://docs.pictify.io/api-reference/endpoints/gifs/list
get /gif
# Create Canvas Image
Source: https://docs.pictify.io/api-reference/endpoints/images/canvas
post /image/canvas
Render an image from FabricJS canvas data
# Generate Image
Source: https://docs.pictify.io/api-reference/endpoints/images/generate
post /image
Generate an image from HTML, URL, or template
# Get Image
Source: https://docs.pictify.io/api-reference/endpoints/images/get
get /image/{uid}
# List Images
Source: https://docs.pictify.io/api-reference/endpoints/images/list
get /image
Get a paginated list of generated images
# Render Multi-Page PDF
Source: https://docs.pictify.io/api-reference/endpoints/pdfs/multi-page
post /pdf/multi-page
Generate a PDF with multiple pages from a template
# Get PDF Presets
Source: https://docs.pictify.io/api-reference/endpoints/pdfs/presets
get /pdf/presets
Get available PDF size presets
# Render PDF
Source: https://docs.pictify.io/api-reference/endpoints/pdfs/render
post /pdf/render
Generate a single-page PDF from a template
# Create Template
Source: https://docs.pictify.io/api-reference/endpoints/templates/create
post /templates
Create a template with either the Handlebars HTML engine or the FabricJS canvas engine.
Pictify templates come in two flavours. Pick one with the `engine` field.
| Engine | Authoring surface | Use it when |
| -------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| `html` | Raw HTML + Handlebars (`{{var}}`, `{{#each}}`, `{{#if}}`, helpers) | You want full control over markup, CSS, and copy-paste-able source. |
| `fabric` *(default)* | FabricJS canvas JSON | You're exporting from the dashboard editor or automating canvas layouts. |
If you omit `engine`, the API defaults to `fabric` for backwards compatibility. New
HTML templates should set `engine: "html"` explicitly.
## Handlebars HTML templates
Set `engine: "html"` and pass your template source in `html`. Pictify compile-validates
it on save and fails with HTTP 422 if a block is unclosed or a helper is unknown.
```bash cURL theme={null}
curl -X POST https://api.pictify.io/templates \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "OG Image",
"engine": "html",
"width": 1200,
"height": 630,
"html": "
{{title}}
{{subtitle}}
{{#if showCta}}{{/if}}
",
"variableDefinitions": [
{ "name": "title", "type": "text", "defaultValue": "Hello world" },
{ "name": "subtitle", "type": "text" },
{ "name": "ctaLabel", "type": "text", "defaultValue": "Learn more" },
{ "name": "ctaText", "type": "text", "defaultValue": "Learn more" }
]
}'
```
### Auto-added variables
Any `{{identifier}}` referenced in the template body that is **not** declared in
`variableDefinitions` is automatically added as a text variable on save. The response
echoes the added names under `addedVariables` so your client can surface them:
```json theme={null}
{
"template": {
"uid": "tmpl_...",
"engine": "html",
"variableDefinitions": [
{ "name": "title", "type": "text" },
{ "name": "price", "type": "text" }
]
},
"addedVariables": ["price"]
}
```
This is what makes the `engine=html` authoring loop feel "just work" — you can type
`{{price}}` into your template and save; the variable appears on the next read
without a separate declaration step.
### `strictVariables` and `jsEnabled`
Two HTML-only toggles affect render behaviour:
When `true`, rendering fails with HTTP 422 if a root-level variable referenced in the
template was not supplied. Leave off if you rely on `{{#if optional}}` guards.
When `true`, scripts inside the template execute during render (Chart.js, KaTeX,
animated SVG). Off by default to prevent runaway loops. A 30s hard timeout always applies.
Both default to `false` and can be flipped with `PUT /templates/{uid}`.
### Helpers and expressions
HTML templates have access to the full Pictify helper library — string casing,
number and currency formatting, date formatting, array helpers, and JSON
inspection. See [Handlebars syntax in Expressions](/concepts/expressions#handlebars-syntax-html-templates) for block syntax and the full helper library.
```html theme={null}
{{/each}}
```
## FabricJS canvas templates
Set `engine: "fabric"` (or omit — it's the default) and pass a FabricJS canvas JSON
object in `fabricJSData`. Multi-page canvases are supported via `pages`.
```bash cURL theme={null}
curl -X POST https://api.pictify.io/templates \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Quote card",
"engine": "fabric",
"width": 1080,
"height": 1080,
"fabricJSData": { "version": "5.3.0", "objects": [] }
}'
```
Base64 `data:image/...` sources inside `fabricJSData` are uploaded to Pictify storage
during save and replaced with CDN URLs. No extra step needed.
## Rendering
Once saved, render with [`POST /templates/{uid}/render`](/api-reference/endpoints/templates/render)
and pass variable values in the `variables` object.
```bash cURL theme={null}
curl -X POST https://api.pictify.io/templates/tmpl_abc123/render \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"variables": { "title": "Launch day", "subtitle": "Ship it" },
"format": "png"
}'
```
# Delete Template
Source: https://docs.pictify.io/api-reference/endpoints/templates/delete
delete /templates/{uid}
# Get Template
Source: https://docs.pictify.io/api-reference/endpoints/templates/get
get /templates/{uid}
# List Templates
Source: https://docs.pictify.io/api-reference/endpoints/templates/list
get /templates
# Render Template
Source: https://docs.pictify.io/api-reference/endpoints/templates/render
post /templates/{uid}/render
Generate an image from a template with variables
# Update Template
Source: https://docs.pictify.io/api-reference/endpoints/templates/update
put /templates/{uid}
# Get Template Variables
Source: https://docs.pictify.io/api-reference/endpoints/templates/variables
get /templates/{uid}/variables
# Create Webhook
Source: https://docs.pictify.io/api-reference/endpoints/webhooks/create
post /webhook-subscriptions
# Delete Webhook
Source: https://docs.pictify.io/api-reference/endpoints/webhooks/delete
delete /webhook-subscriptions/{uid}
# Get Webhook
Source: https://docs.pictify.io/api-reference/endpoints/webhooks/get
get /webhook-subscriptions/{uid}
# List Webhooks
Source: https://docs.pictify.io/api-reference/endpoints/webhooks/list
get /webhook-subscriptions
# Update Webhook
Source: https://docs.pictify.io/api-reference/endpoints/webhooks/update
put /webhook-subscriptions/{uid}
# Error Handling
Source: https://docs.pictify.io/api-reference/errors
API error codes and troubleshooting
# Error Handling
The Pictify API uses standard HTTP status codes with plain JSON error bodies.
## Error Response Format
Errors carry a human-readable message under one of two keys — `message` (most endpoints) or `error` (some rendering endpoints). Handle both:
```json theme={null}
{
"message": "Template not found"
}
```
```json theme={null}
{
"error": "templateUid is required"
}
```
Two extensions appear on specific endpoints:
* **`code`** — a stable machine-readable slug on newer endpoints (video, workflows, stock): `quota_exceeded`, `template_limit_reached`, `ai_unavailable`, `stock_unavailable`, `invalid_variable`, `preview_not_supported`.
```json theme={null}
{
"message": "You've used all your renders for this month. Upgrade your plan to unlock more, or hang tight until next month!",
"code": "quota_exceeded"
}
```
* **`errors`** — an array of strings when a template fails to compile (video code templates return `422` with every compiler error at once):
```json theme={null}
{
"errors": [
"UserScene.tsx: 'styled-components' is not an allowed import",
"schema must export a default for every field"
]
}
```
```javascript theme={null}
// A tolerant reader that covers every Pictify error shape
const readError = (body) =>
body?.message || body?.error || (body?.errors || []).join('; ') || 'Request failed';
```
## HTTP Status Codes
| Code | Meaning | What to do |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `200 OK` | Request succeeded | — |
| `400 Bad Request` | Invalid parameters | Fix the request; the message names the field |
| `401 Unauthorized` | Missing or invalid API key | Check the `Authorization: Bearer` header |
| `402 Payment Required` | Credits exhausted (video/workflow endpoints), or a plan limit reached | Upgrade, or wait for the monthly reset |
| `404 Not Found` | Resource doesn't exist or belongs to another team | Check the uid |
| `422 Unprocessable Entity` | Semantically invalid (bad variables, compile errors) | Read `message` or `errors[]` |
| `429 Too Many Requests` | `code: "quota_exceeded"` → monthly credits exhausted (image/GIF/template/batch endpoints); otherwise a per-minute rate limit | Quota: upgrade or wait for the reset. Rate: back off and retry |
| `500 Internal Server Error` | Unexpected server error | Retry once, then contact support |
| `501 Not Implemented` | Feature not configured on this server | The message explains what's missing |
| `502 Bad Gateway` | An upstream service (AI, transcription) failed | Retry |
Authentication failures return `401` with the body `{ "message": "Invalid Request" }` — deliberately unspecific, so a probing caller cannot distinguish a revoked key from a malformed one.
## Common Errors
### Out of credits
Every render — image, GIF, PDF page, video — consumes monthly credits. When they run out, the video and workflow endpoints return `402`; the image, GIF, HTML-template and batch endpoints return `429` — both with the same body:
```json theme={null}
{
"message": "You've used all your renders for this month. Upgrade your plan to unlock more, or hang tight until next month!",
"code": "quota_exceeded"
}
```
### Template not found (404)
Templates are scoped to your team. A valid uid owned by a different team returns `404`, not `403`.
### Invalid variables (422)
Rendering with variables that violate the template's declared definitions:
```json theme={null}
{
"message": "Variable \"recipientName\" is required and was not provided.",
"code": "invalid_variable"
}
```
## Retries
On a `429` **check `code` first**: `quota_exceeded` is a monthly limit that no retry refills. Rate-limit 429s (public rendering, previews) send standard `x-ratelimit-*` and `retry-after` headers — honour those. On `5xx`, one immediate retry is safe — all generation endpoints are idempotent from your side (a failed request bills nothing).
# HTML to GIF API
Source: https://docs.pictify.io/api-reference/generation/gifs
post /gif
Create animated GIFs from HTML/CSS animations or live URL captures — set duration and fps, get a CDN-hosted GIF
# GIF Generation
Generate animated GIFs from HTML with CSS animations or by recording live web pages.
## Endpoints
| Method | Endpoint | Description |
| ------ | -------------- | -------------------------------------------------------------------------------- |
| `POST` | `/gif` | [Create GIF](/api-reference/endpoints/gifs/create) from HTML with CSS animations |
| `GET` | `/gif` | [List GIFs](/api-reference/endpoints/gifs/list) |
| `GET` | `/gif/{uid}` | [Get GIF](/api-reference/endpoints/gifs/get) |
| `POST` | `/gif/capture` | [Capture GIF](/api-reference/endpoints/gifs/capture) from a live URL |
## Quality Presets
| Preset | Frame Rate | Max Duration | Best For |
| -------- | ---------- | ------------ | ----------------------------------- |
| `low` | 10 fps | 10 seconds | Small file size, simple animations |
| `medium` | 15 fps | 15 seconds | Balanced quality and size |
| `high` | 24 fps | 30 seconds | Smooth animations, detailed content |
The response is wrapped in a `gif` object. Higher quality settings produce larger files.
## Key Parameters
| Parameter | Type | Default | Description |
| ---------------------- | ------- | -------- | ----------------------------------------------------------- |
| `width` | integer | - | Output width (1-2000, required) |
| `height` | integer | - | Output height (1-2000, required) |
| `quality` | string | `medium` | Quality preset |
| `frameDurationSeconds` | number | - | Animation duration to capture |
| `selector` | string | - | CSS selector to capture specific element (URL capture only) |
| `waitForSelector` | string | - | Wait for element before recording (URL capture only) |
## Tips for Better GIFs
1. **Keep dimensions reasonable** -- smaller GIFs load faster and compress better
2. **Optimize animations** -- simple, smooth animations compress better than complex ones
3. **Use solid backgrounds** -- gradients and transparency increase file size
4. **Limit colors** -- GIFs are limited to 256 colors per frame
5. **Loop seamlessly** -- design animations to loop smoothly for better visual appeal
# HTML to Image API
Source: https://docs.pictify.io/api-reference/generation/images
post /image
Convert HTML to PNG, JPG, or WebP images via REST API — from raw HTML, a URL screenshot, or a saved template
# Image Generation
Generate PNG, JPEG, or WebP images from HTML content, URLs, or saved templates. You can also render FabricJS canvas data directly.
One of `html`, `url`, or `template` is required. See each endpoint page for full request/response details.
## Endpoints
| Method | Endpoint | Description |
| ------ | --------------- | -------------------------------------------------------------------------------------- |
| `POST` | `/image` | [Generate image](/api-reference/endpoints/images/generate) from HTML, URL, or template |
| `GET` | `/image` | [List images](/api-reference/endpoints/images/list) |
| `GET` | `/image/{uid}` | [Get image](/api-reference/endpoints/images/get) |
| `POST` | `/image/canvas` | [Canvas render](/api-reference/endpoints/images/canvas) from FabricJS data |
## Input Sources
| Source | Parameter | Description |
| -------- | -------------- | ---------------------------------------- |
| HTML | `html` | Render raw HTML/CSS content |
| URL | `url` | Screenshot a publicly accessible URL |
| Template | `template` | Render a saved template with `variables` |
| Canvas | `fabricJSData` | Render FabricJS canvas JSON export |
## Output Formats
| Format | Extension | Best For |
| ------ | -------------- | ---------------------------------------- |
| PNG | `png` | Default, lossless, supports transparency |
| JPEG | `jpg` / `jpeg` | Smaller file size, photos |
| WebP | `webp` | Modern format, best compression |
Use the `fileExtension` parameter to change the output format.
## Key Parameters
| Parameter | Type | Default | Description |
| --------------- | ------- | ------- | ------------------------------------------ |
| `width` | integer | 1200 | Output width in pixels (1-4000) |
| `height` | integer | 630 | Output height in pixels (1-4000) |
| `selector` | string | - | CSS selector to capture a specific element |
| `fileExtension` | string | `png` | Output format |
The `userStorageUrl` response field is only included if you have configured custom storage in your account settings.
# PDF Generation API
Source: https://docs.pictify.io/api-reference/generation/pdfs
post /pdf/render
Generate single and multi-page PDFs from HTML templates — invoices, certificates, and reports with variables per render
# PDF Generation
Generate PDFs from templates with support for multiple pages, standard paper sizes, and custom dimensions.
## Endpoints
| Method | Endpoint | Description |
| ------ | ----------------- | ------------------------------------------------------------------------------------------ |
| `POST` | `/pdf/render` | [Render PDF](/api-reference/endpoints/pdfs/render) from a template |
| `POST` | `/pdf/multi-page` | [Multi-page PDF](/api-reference/endpoints/pdfs/multi-page) with different content per page |
| `GET` | `/pdf/presets` | [List presets](/api-reference/endpoints/pdfs/presets) for paper sizes |
## Paper Size Presets
| Preset | Dimensions | Description |
| -------- | ------------ | ----------------------------------- |
| `A4` | 210 x 297 mm | ISO standard, most common worldwide |
| `Letter` | 8.5 x 11 in | US standard letter size |
| `Legal` | 8.5 x 14 in | US legal size |
| `A3` | 297 x 420 mm | Large format |
| `A5` | 148 x 210 mm | Compact size, booklets |
| `custom` | User-defined | Specify with `customSize` |
## Key Options
| Parameter | Type | Default | Description |
| ----------------- | ------- | ------- | --------------------------------------------------------- |
| `preset` | string | `A4` | Paper size preset |
| `landscape` | boolean | `false` | Landscape orientation |
| `margins` | object | - | Page margins (`top`, `bottom`, `left`, `right`) in pixels |
| `customSize` | object | - | Custom dimensions when preset is `custom` |
| `printBackground` | boolean | `true` | Include CSS backgrounds |
| `scale` | number | `1` | Scale factor (0.1-2.0) |
The `userStorageUrl` response field is only included if you have configured custom storage.
## Best Practices
1. **Design for print** -- use CMYK-friendly colors and sufficient margins
2. **Test at scale** -- preview PDFs at 100% zoom before production
3. **Optimize images** -- use appropriate resolution (150-300 DPI for print)
4. **Consider page breaks** -- design templates with natural page breaks for multi-page docs
5. **Use web-safe fonts** -- or embed custom fonts in your template
# API Overview
Source: https://docs.pictify.io/api-reference/overview
Pictify API reference and conventions
# API Overview
The Pictify API is a RESTful JSON API for generating images, GIFs, and PDFs programmatically.
## Base URL
```
https://api.pictify.io
```
## Authentication
All requests require a Bearer token:
```bash theme={null}
curl https://api.pictify.io/image \
-H "Authorization: Bearer YOUR_API_KEY"
```
See [Authentication](/authentication) for details.
## Request Format
* Content-Type: `application/json`
* Request bodies are JSON
* Dates use ISO 8601 format
```bash theme={null}
curl -X POST https://api.pictify.io/image \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "
Hello World
",
"width": 1200,
"height": 630
}'
```
## Response Format
Successful responses return JSON with relevant data:
```json theme={null}
{
"url": "https://cdn.pictify.io/renders/abc123.png",
"id": "img_abc123",
"width": 1200,
"height": 630,
"createdAt": "2026-01-29T10:30:00Z"
}
```
Error responses are plain JSON with a human-readable message under `message` or `error` (see [Error Handling](/api-reference/errors) for every shape):
```json theme={null}
{
"message": "Template not found"
}
```
## Rate Limits and Credits
Two separate limits apply, and — for historical reasons — they surface under different status codes per endpoint family:
* **Monthly credits** — every render (image, GIF, PDF page, video) consumes credits. When they run out, the **image, GIF, HTML-template and batch endpoints return `429`** with `code: "quota_exceeded"`, while the **video and workflow endpoints return `402`** with the same code. Treat `quota_exceeded` as "wait for the monthly reset or upgrade" — never as "retry with backoff": no amount of retrying refills a monthly quota.
* **Per-endpoint rate limits** — a few endpoints (unauthenticated public rendering, template previews) carry fixed per-minute limits. These return `429` *without* a `quota_exceeded` code and DO send standard `x-ratelimit-*` headers and `retry-after` — those are genuinely retryable after backing off.
So the rule for `429` is: **check the `code` field first.** `quota_exceeded` means credits, not rate.
## Pagination
List endpoints return paginated results:
```bash theme={null}
curl "https://api.pictify.io/templates?page=2&limit=20" \
-H "Authorization: Bearer $API_KEY"
```
Response includes pagination metadata:
```json theme={null}
{
"templates": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 45,
"totalPages": 3,
"hasNext": true,
"hasPrev": true
}
}
```
## HTTP Status Codes
| Code | Description |
| ---- | ---------------------------------- |
| 200 | Success |
| 201 | Created |
| 202 | Accepted (async operation started) |
| 400 | Bad Request (validation error) |
| 401 | Unauthorized (invalid API key) |
| 403 | Forbidden (access denied) |
| 404 | Not Found |
| 422 | Unprocessable Entity |
| 429 | Rate Limit Exceeded |
| 500 | Internal Server Error |
## Idempotency
POST requests can include an `Idempotency-Key` header for safe retries:
```bash theme={null}
curl -X POST https://api.pictify.io/image \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: unique-request-id-123" \
-H "Content-Type: application/json" \
-d '{"html": "..."}'
```
The same key with the same request will return the cached response for 24 hours.
## Versioning
The API does not currently use URL-based versioning. All endpoints are accessed directly from the base URL. Breaking changes will be communicated in advance via release notes.
## SDK Libraries
Official SDKs handle authentication, retries, and error handling:
* [Node.js SDK](/sdks/nodejs)
* [Python SDK](/sdks/python)
* [Go SDK](/sdks/go)
* [Ruby SDK](/sdks/ruby)
## Endpoints
### Generation
| Endpoint | Method | Description |
| ------------------------- | ------ | -------------------------------------------------------------------------- |
| `/image` | POST | [Generate an image](/api-reference/generation/images) |
| `/image/canvas` | POST | [Image from FabricJS canvas](/api-reference/generation/images#canvas) |
| `/image/agent-screenshot` | POST | [AI-powered screenshot](/api-reference/generation/images#agent-screenshot) |
| `/templates/{uid}/render` | POST | [Render template to image](/api-reference/templates#render) |
| `/gif` | POST | [Generate a GIF](/api-reference/generation/gifs) |
| `/gif/capture` | POST | [Capture GIF from URL](/api-reference/generation/gifs#capture) |
| `/pdf/render` | POST | [Generate a PDF](/api-reference/generation/pdfs) |
| `/pdf/multi-page` | POST | [Multi-page PDF](/api-reference/generation/pdfs#multi-page) |
### Templates
| Endpoint | Method | Description |
| ---------------------------- | ------ | --------------------------------------------------- |
| `/templates` | GET | [List templates](/api-reference/templates#list) |
| `/templates` | POST | [Create template](/api-reference/templates#create) |
| `/templates/{uid}` | GET | [Get template](/api-reference/templates#get) |
| `/templates/{uid}` | PUT | [Update template](/api-reference/templates#update) |
| `/templates/{uid}` | DELETE | [Delete template](/api-reference/templates#delete) |
| `/templates/{uid}/render` | POST | [Render template](/api-reference/templates#render) |
| `/templates/{uid}/variables` | GET | [Get variables](/api-reference/templates#variables) |
### Batch Operations
| Endpoint | Method | Description |
| ------------------------------- | ------ | ------------------------------------------------- |
| `/templates/{uid}/batch-render` | POST | [Start batch job](/api-reference/batch#create) |
| `/templates/batch/{id}/results` | GET | [Get batch results](/api-reference/batch#results) |
| `/templates/batch/{id}/cancel` | POST | [Cancel batch](/api-reference/batch#cancel) |
### Webhooks
| Endpoint | Method | Description |
| ------------------------------ | ------ | ----------------------------------------------------- |
| `/webhook-subscriptions` | GET | [List subscriptions](/api-reference/webhooks#list) |
| `/webhook-subscriptions` | POST | [Create subscription](/api-reference/webhooks#create) |
| `/webhook-subscriptions/{uid}` | GET | [Get subscription](/api-reference/webhooks#get) |
| `/webhook-subscriptions/{uid}` | PUT | [Update subscription](/api-reference/webhooks#update) |
| `/webhook-subscriptions/{uid}` | DELETE | [Delete subscription](/api-reference/webhooks#delete) |
### Bindings
| Endpoint | Method | Description |
| ----------------- | ------ | ------------------------------------------------ |
| `/bindings` | GET | [List bindings](/api-reference/bindings#list) |
| `/bindings` | POST | [Create binding](/api-reference/bindings#create) |
| `/bindings/{uid}` | GET | [Get binding](/api-reference/bindings#get) |
| `/bindings/{uid}` | PUT | [Update binding](/api-reference/bindings#update) |
| `/bindings/{uid}` | DELETE | [Delete binding](/api-reference/bindings#delete) |
# Templates
Source: https://docs.pictify.io/api-reference/templates
get /templates
Create, manage, and render reusable templates
# Templates
Templates are reusable designs with dynamic variables. Create once, render with different data.
For expression syntax and conditional rendering, see [Expressions](/concepts/expressions).
## Endpoints
| Method | Endpoint | Description |
| -------- | ---------------------------- | ----------------------------------------------------------------------------------- |
| `GET` | `/templates` | [List templates](/api-reference/endpoints/templates/list) |
| `POST` | `/templates` | [Create template](/api-reference/endpoints/templates/create) |
| `GET` | `/templates/{uid}` | [Get template](/api-reference/endpoints/templates/get) |
| `PUT` | `/templates/{uid}` | [Update template](/api-reference/endpoints/templates/update) |
| `DELETE` | `/templates/{uid}` | [Delete template](/api-reference/endpoints/templates/delete) |
| `POST` | `/templates/{uid}/render` | [Render template](/api-reference/endpoints/templates/render) to an image |
| `GET` | `/templates/{uid}/variables` | [Get variables](/api-reference/endpoints/templates/variables) defined in a template |
## Variable Types
Templates support different variable types in `{{variable}}` placeholders:
| Type | Example | Description |
| --------- | --------------- | --------------------- |
| `string` | `"Hello World"` | Text content |
| `number` | `42`, `3.14` | Numeric values |
| `boolean` | `true`, `false` | Conditional rendering |
| `array` | `["a", "b"]` | Lists for iteration |
| `object` | `{name: "..."}` | Nested data |
## Using Variables in Templates
### Simple Interpolation
```html theme={null}
```
See [Expressions](/concepts/expressions) for the full expression syntax.
## Layout Variants
Templates support multiple layout variants for different platforms. Each layout stores a separate canvas design optimized for a specific size (e.g., Twitter 1200x675, Instagram 1080x1080).
* Layouts are created via the **AI Resize** feature in the editor
* Variables are shared across all layouts
* Render a specific layout with the `layout` parameter, or multiple with `layouts`
* The `default` layout key refers to the base template
```bash theme={null}
# Render specific layout
curl -X POST /templates/{uid}/render \
-d '{"variables": {"title": "Hello"}, "layout": "twitter-post"}'
# Render multiple layouts
curl -X POST /templates/{uid}/render \
-d '{"variables": {"title": "Hello"}, "layouts": ["default", "twitter-post", "facebook-post"]}'
```
See [Rendering - Layout Variants](/concepts/rendering#layout-variants) for details.
## Template Content
A template requires either `html` or `fabricJSData` (FabricJS canvas JSON), but not both.
Either `html` or `fabricJSData` is required when creating a template. You cannot provide both.
# URL Rendering
Source: https://docs.pictify.io/api-reference/url-rendering
A template as an image URL — variables in the query string, rendered on request
# URL Rendering
The most embeddable way to use a template: a plain `GET` URL that returns the rendered image. Put it in an `` tag, an email, a Notion page, a README — anywhere a URL can go, with the variables riding in the query string.
```
GET https://api.pictify.io/r/{templateUid}.{format}?token=YOUR_API_KEY&name=Maya&score=98
```
```html theme={null}
```
Every query parameter except the reserved ones below is passed to the template as a variable — `?name=Maya` fills `{{name}}`.
## Formats and parameters
| Part | Values |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `.{format}` | `png`, `jpeg`, `jpg`, `webp` |
| `token` | Your API key. Query auth exists because an `` tag cannot send headers; the `Authorization: Bearer` header also works and is preferred anywhere you control the request |
| `quality` | `0`–`1`, default `0.9` (jpeg/webp) |
| `layout` | A layout key, when the template declares alternate layouts |
| everything else | Template variables |
## Behaviour worth knowing
* **Failures return a transparent 1x1 PNG**, not an error page — a broken variable never renders a broken image into your email or README. Check the dashboard if a URL renders blank.
* Responses are cached privately for 5 minutes (`Cache-Control: private, max-age=300`), so a URL hit repeatedly by the same client re-renders at most every 5 minutes.
* Each **unique render** is metered as one image credit.
A URL containing `token=` is a credential. Use it where the URL stays private (emails to known recipients, internal tools). For public pages, render ahead of time with `POST /templates/:uid/render` and embed the CDN URL it returns — that URL is not a credential and never re-renders.
# Video Generation API
Source: https://docs.pictify.io/api-reference/video
Programmatic MP4 video from templates — render, generate with AI, and manage over the same API token
# Video Templates API
Render MP4 video the same way you render images: build a **video template** once, then render it with different variables per request. Templates come in two kinds — **timeline** templates built in the visual studio, and **code** templates written as single-file Remotion (React) scenes — and both render through the same endpoint.
All endpoints live under `/video` and accept your normal API key as a Bearer token.
Video renders are long requests: expect up to a few minutes for a full render. The request waits and returns the finished MP4 URL. Each render consumes credits like any other generation.
## List your video templates
```bash theme={null}
curl https://api.pictify.io/video/templates \
-H "Authorization: Bearer YOUR_API_KEY"
```
Returns `{ "templates": [...] }` with each template's `uid`, `kind`, dimensions, fps, duration and variable definitions — everything you need to render it.
## Render a template — MP4 or GIF
```bash theme={null}
curl -X POST https://api.pictify.io/video/templates/TEMPLATE_UID/render \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{
"variables": {
"title": "Welcome, Maya!",
"accentColor": "#ff5533"
},
"format": "mp4"
}'
```
`format` is `"mp4"` (default) or `"gif"` — the same template and the same render, encoded for places an MP4 cannot autoplay. Timeline templates are palette-converted and capped at 15fps / 720px wide so files stay shareable; code (tsx) templates encode GIF natively at half the composition's frame rate with no width cap.
Response:
```json theme={null}
{
"url": "https://media.pictify.io/videos/....mp4",
"durationInFrames": 240,
"format": "mp4"
}
```
For timeline templates, variable VALUES are validated against the declared definitions — a malformed value (bad colour, unsafe URL, missing required) returns `422` with `code: "invalid_variable"`, while unknown variable names are ignored by design. Code (tsx) templates receive variables directly as component props with no server-side validation — the scene's zod defaults govern what renders.
## Get a template's variables
```bash theme={null}
curl https://api.pictify.io/video/templates/TEMPLATE_UID/variables \
-H "Authorization: Bearer YOUR_API_KEY"
```
Use this to build forms or map spreadsheet columns before rendering.
## Generate a template with AI
Describe the video; the API designs a motion brief, writes the scene code, compiles it, renders preview frames and reviews them visually — then returns a draft template ready to render or refine in the studio.
```bash theme={null}
curl -X POST https://api.pictify.io/video/templates/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{
"prompt": "An 8 second product launch teaser for a developer tool called ShipFast — dark, electric, type-driven",
"width": 1080,
"height": 1080,
"durationSeconds": 8,
"brandColor": "#00ff41"
}'
```
Response: `{ "template": {...}, "previewUrl": "https://..." }` — the template's schema fields (texts, colours, optional image) are derived from the generated scene, so the result is immediately parameterisable.
Generation takes 30–60 seconds and is metered as one render.
## Preview a single frame
```bash theme={null}
curl -X POST https://api.pictify.io/video/templates/TEMPLATE_UID/preview \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{ "variables": {}, "frame": 45 }'
```
Renders one frame to PNG — a cheap way to check a variable set before committing to a full video render. Metered as one image. Code (tsx) templates only: timeline templates return `501` with `code: "preview_not_supported"` (the studio previews them live in the browser instead).
## Template management
| Method | Path | Purpose |
| -------- | --------------------------------- | -------------------------------------------------------- |
| `POST` | `/video/templates` | Create a template (timeline `projectJson` or code `tsx`) |
| `GET` | `/video/templates/:uid` | Fetch one template |
| `PUT` | `/video/templates/:uid` | Update — code templates are recompiled and re-validated |
| `DELETE` | `/video/templates/:uid` | Delete |
| `POST` | `/video/templates/:uid/duplicate` | Copy — the natural way to make a variant |
Code (`tsx`) templates pass a compile gate on every create and update: imports are restricted to `remotion` (including `@remotion/*` subpackages), `react` and `zod`, and a template that does not build is rejected with `422` and the compiler errors, so a template that saves is a template that renders.
# Webhook Subscriptions
Source: https://docs.pictify.io/api-reference/webhooks
get /webhook-subscriptions
Manage webhook subscriptions via API
# Webhook Subscriptions
Manage webhook subscriptions programmatically. Webhooks notify your server when events occur in your Pictify account.
For webhook payload formats and signature verification, see [Webhooks Concept](/concepts/webhooks).
## Endpoints
| Method | Endpoint | Description |
| -------- | ------------------------------ | --------------------------------------------------------------- |
| `GET` | `/webhook-subscriptions` | [List subscriptions](/api-reference/endpoints/webhooks/list) |
| `POST` | `/webhook-subscriptions` | [Create subscription](/api-reference/endpoints/webhooks/create) |
| `GET` | `/webhook-subscriptions/{uid}` | [Get subscription](/api-reference/endpoints/webhooks/get) |
| `PUT` | `/webhook-subscriptions/{uid}` | [Update subscription](/api-reference/endpoints/webhooks/update) |
| `DELETE` | `/webhook-subscriptions/{uid}` | [Delete subscription](/api-reference/endpoints/webhooks/delete) |
The `secret` is only returned when creating a subscription. Store it securely for signature verification.
## Supported Events
| Event | Description |
| ------------------ | ----------------------------------------------- |
| `render.completed` | Image, GIF, or PDF render finished successfully |
| `render.failed` | Render failed with an error |
| `binding.updated` | Binding data refreshed |
| `binding.failed` | Binding data fetch failed |
## Filters
Filter webhooks to only receive specific events by template or render type:
```json theme={null}
{
"filters": {
"templateId": "tmpl_abc123",
"type": "image"
}
}
```
## Subscription Status
| Status | Description |
| -------- | ----------------------------------------- |
| `active` | Receiving webhooks |
| `paused` | Temporarily disabled |
| `failed` | Disabled after repeated delivery failures |
## Delivery Information
The `lastDelivery` field on a subscription shows the most recent delivery attempt, including `status`, `timestamp`, `responseCode`, and retry information for failures.
# Workflows
Source: https://docs.pictify.io/api-reference/workflows
CSV or webhook in, branded documents out — batch runs with per-recipient delivery
# Workflows API
A workflow turns rows of data into rendered, delivered documents: point it at a spreadsheet or push rows to its inbound hook, and every row becomes a branded certificate, badge or report — rendered from your template and optionally emailed to its recipient.
Workflows are built in the [dashboard](https://pictify.io/dashboard/workflows/new); the API surface below is how external systems feed and observe them.
## Inbound hooks
Every workflow can expose an **inbound hook** — a public URL that accepts rows from any system that can send JSON (Zapier, n8n, your backend, a form provider):
```bash theme={null}
curl -X POST https://api.pictify.io/workflow/hooks/HOOK_UID/HOOK_SECRET \
-H "Content-Type: application/json" \
--data-raw '{
"recipientName": "Maya Chen",
"recipientEmail": "maya@example.com",
"courseName": "Advanced TypeScript"
}'
```
The random secret in the URL authenticates the caller. Each accepted row is rendered with the workflow's template and delivered per the workflow's settings.
### Optional HMAC signing
A URL travels through proxy logs, browser history and referrers — so for higher assurance, enable **require signature** on the hook. The sender must then also sign the exact raw request body:
```
X-Pictify-Signature: t=,v1=
```
where `v1 = HMAC_SHA256(signingSecret, t + "." + rawBody)`. With signing on, a leaked URL alone is no longer enough to inject rows; the signature also rules out payload tampering and replay (timestamps outside the tolerance window are rejected).
```javascript theme={null}
import crypto from 'node:crypto';
const t = Math.floor(Date.now() / 1000);
const body = JSON.stringify(row);
const v1 = crypto.createHmac('sha256', SIGNING_SECRET).update(`${t}.${body}`).digest('hex');
await fetch(hookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Pictify-Signature': `t=${t},v1=${v1}` },
body, // send EXACTLY the bytes you signed
});
```
Sign the exact bytes you send. Re-serialising JSON after signing changes key order or whitespace and invalidates the signature.
## Runs and stats
| Method | Path | Purpose |
| ------ | ------------------- | ------------------------------------------------------------------- |
| `GET` | `/workflow` | List recent RUNS (newest 20) |
| `GET` | `/workflow/:uid` | One run, with its per-row items |
| `GET` | `/workflow/stats` | Totals: runs, documents rendered, documents delivered |
| `POST` | `/workflow/preview` | Preview a template render with a sample row before committing a run |
A run reports per-row progress — `counts: { total, rendered, delivered, failed }` — so a caller can poll a run until `rendered === total` or surface partial failures precisely.
## Hook management
| Method | Path | Purpose |
| -------- | ---------------------- | -------------------------------------------------------------- |
| `POST` | `/workflow/hooks` | Create a hook for a workflow (returns the URL with its secret) |
| `GET` | `/workflow/hooks` | List hooks |
| `DELETE` | `/workflow/hooks/:uid` | Revoke — the URL stops accepting rows immediately |
All management endpoints authenticate with your normal API key as a Bearer token; only the inbound hook URL itself is public.
# Authentication
Source: https://docs.pictify.io/authentication
Secure your API requests with API keys
# Authentication
All API requests require authentication using an API key passed in the `Authorization` header.
## API Keys
API keys are created in your [dashboard settings](https://pictify.io/dashboard/settings). Each key is associated with your team and has access to all team resources.
### Creating an API Key
1. Navigate to **Settings** > **API Keys** in the dashboard
2. Click **Create API Key**
3. Give your key a descriptive name (e.g., "Production Server", "Development")
4. Copy the key immediately - it won't be shown again
API keys provide full access to your account. Keep them secure and never expose them in client-side code.
## Using Your API Key
Include your API key in the `Authorization` header as a Bearer token:
```bash theme={null}
curl -X POST https://api.pictify.io/image \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "
Hello
"}'
```
### SDK Configuration
```typescript Node.js theme={null}
import { Pictify } from '@pictify/sdk';
const pictify = new Pictify({
apiKey: process.env.PICTIFY_API_KEY
});
```
```python Python theme={null}
from pictify import Pictify
client = Pictify(api_key=os.environ["PICTIFY_API_KEY"])
```
```go Go theme={null}
client := pictify.NewClient(os.Getenv("PICTIFY_API_KEY"))
```
```ruby Ruby theme={null}
client = Pictify::Client.new(api_key: ENV['PICTIFY_API_KEY'])
```
## Key Format
An API key is a single 64-character hex string — there are no key prefixes, key types, or sandbox keys. Every key is a live key: requests made with it render real assets and count against your plan's monthly credits.
To test without spending credits, use the free tier's included credits or the [API Playground](https://pictify.io/dashboard/api-playground), which shows the exact request and response for every endpoint.
## Security Best Practices
### Environment Variables
Never hardcode API keys. Use environment variables:
```bash theme={null}
# .env
PICTIFY_API_KEY=your-64-character-api-key
```
### Server-Side Only
API keys should only be used in server-side code. Never include them in:
* Client-side JavaScript
* Mobile apps
* Public repositories
* Browser localStorage/cookies
### Key Rotation
If you suspect a key has been compromised:
1. Create a new API key in the dashboard
2. Update your application to use the new key
3. Delete the compromised key
### Least Privilege
Create separate API keys for different environments and services:
* Production server
* Staging server
* CI/CD pipeline
* Local development
## Rate Limits and Quotas
Two separate limits apply, and — for historical reasons — they surface under different status codes per endpoint family:
* **Monthly credits** — every render (image, GIF, PDF, video) consumes render credits; see [Credits](#credits) below for what each operation costs. When they run out, the **image, GIF, HTML-template and batch endpoints return `429`** with `code: "quota_exceeded"`, while the **video and workflow endpoints return `402`** with the same code. Treat `quota_exceeded` as "wait for the monthly reset or upgrade" — never as "retry with backoff": no amount of retrying refills a monthly quota.
* **Per-endpoint rate limits** — a few endpoints (unauthenticated public rendering, template previews) carry fixed per-minute limits. These return `429` *without* a `quota_exceeded` code and DO send standard `x-ratelimit-*` headers and `retry-after` — those are genuinely retryable after backing off.
So the rule for `429` is: **check the `code` field first.** `quota_exceeded` means credits, not rate.
A quota rejection carries a plain message:
```json theme={null}
{
"message": "You've used all your renders for this month. Upgrade your plan to unlock more, or hang tight until next month!"
}
```
Handle `402` (quota) and `429` (rate) by backing off and surfacing the `message` to your logs — there is no machine-readable retry hint.
## Credits
Your plan carries **two separate monthly pools**. They never mix: renders spend our own rendering compute, while AI operations spend metered third-party AI services, so each is budgeted in its own currency.
### Render credits
Every render costs **1 render credit** — one image, one GIF, one PDF, one video render, one workflow (CSV row or webhook) render, one item in a batch. The single exception is GIF capture from a live URL (`POST /gif/capture`), which costs **1.5 render credits** because it records a headless browser session.
Monthly render allowances per plan are listed on the [pricing page](https://pictify.io/pricing). Paid plans can enable overage billing to keep rendering past the allowance.
### AI credits
All AI operations draw from **one** AI credit pool — there are no separate per-feature AI allowances:
| Operation | Cost |
| ------------------------------------------------------------------------------------------------------ | --------------------------------------- |
| Copilot instruction — template editor or video studio (design, execute and review rounds all included) | 1 AI credit |
| AI code edit of a template | 1 AI credit |
| AI video generation (brief, scene code, compile, visual review, poster) | 5 AI credits |
| Transcription / captions | 1 AI credit per started minute of audio |
Monthly AI credit allowances per plan:
| Plan | AI credits / month |
| ------------- | ------------------ |
| Free | 25 |
| Basic | 300 |
| Pro | 1,000 |
| Business | 4,000 |
| Business Plus | 6,000 |
Billing rules, in your favor:
* **Charged per user intention, not per internal step.** One copilot instruction is 1 credit even if it runs many model rounds internally.
* **Failures are never billed.** Credits are checked before an operation starts and spent only after it succeeds.
* **Silence is never billed.** Transcribing a clip in which no speech is found costs nothing (the request returns `422` with `code: "no_speech"`). When speech is found, minutes are counted up to the last spoken word, not the full clip length.
* **Renders inside AI operations are included.** Review frames and posters generated during an AI operation are part of its AI price — they do not also consume render credits.
AI credits reset on the first of each calendar month. There is no overage billing for AI credits: when the pool is exhausted, AI endpoints return **`402`** with `code: "ai_quota_exceeded"`:
```json theme={null}
{
"message": "You've used this month's AI credits. Upgrade your plan for more, or they reset next month.",
"code": "ai_quota_exceeded"
}
```
Your current balance for both pools is shown on the dashboard's usage meter, and the AI balance is included in the `aiCredits: { used, limit }` field of the plan details endpoint.
## Team API Keys
API keys are scoped to your team. All team members share access to the same API keys and resources.
To manage team members:
1. Go to **Settings** > **Team**
2. Invite members by email
3. Assign roles (Admin, Editor, Viewer)
Only Admins can create, view, and delete API keys.
## Troubleshooting
### Invalid API Key
```json theme={null}
{
"type": "https://docs.pictify.io/errors/invalid-api-key",
"title": "Invalid API Key",
"status": 401,
"detail": "The provided API key is invalid or has been revoked."
}
```
**Solutions:**
* Verify the key is copied correctly (no extra spaces)
* Check if the key has been deleted in the dashboard
* Ensure you're using the correct environment (test vs production)
### Missing Authorization Header
```json theme={null}
{
"type": "https://docs.pictify.io/errors/missing-auth",
"title": "Missing Authentication",
"status": 401,
"detail": "No API key provided. Include your API key in the Authorization header."
}
```
**Solutions:**
* Add the `Authorization: Bearer {api_key}` header
* Check for typos in the header name
# Expressions
Source: https://docs.pictify.io/concepts/expressions
Dynamic logic with the template expression engine
# Expressions
Pictify templates come in two engines, and each has its own syntax for dynamic logic:
| Engine | Syntax style | Example |
| ------------------- | -------------------------------------------------------- | ----------------------------------------------------------------- |
| `html` (Handlebars) | Handlebars blocks + positional helpers | `{{#if isPremium}}...{{/if}}`, `{{currency price 'USD'}}` |
| `fabric` (canvas) | Expression engine — function calls, operators, ternaries | `{{currency(price, 'USD')}}`, `{{isPremium ? 'Gold' : 'Silver'}}` |
Both engines share the **same function library** (the tables below) — only the call style differs. Using expression-style `currency(price, 'USD')` in a Handlebars HTML template (or vice versa) will fail validation.
## Handlebars syntax (HTML templates)
HTML templates (`engine: "html"`) use standard [Handlebars](https://handlebarsjs.com): `{{variable}}` interpolation, block helpers, and positional helper calls. Templates are compile-validated on save — unclosed blocks or unknown helpers fail with HTTP 422.
### Variables and escaping
```handlebars theme={null}
{{title}}
{{user.name}}
```
`{{variable}}` HTML-escapes its output. Raw HTML output with `{{{variable}}}` additionally requires `allowRawHtml: true` on that variable's definition — an XSS guard for user-supplied data.
### Conditionals
```handlebars theme={null}
{{#if isPremium}}
Premium
{{else if isTrial}}
Trial
{{else}}
Free
{{/if}}
{{#unless emailVerified}}
Please verify your email.
{{/unless}}
```
There is no `==` operator and no custom helper registration (helpers are safelisted). For comparisons, pass a precomputed boolean in your render variables — or use the boolean helpers in a subexpression:
```handlebars theme={null}
{{#if (contains roles 'admin')}}Admin panel{{/if}}
{{#if (isEmpty items)}}Nothing here yet.{{/if}}
```
`contains`, `isEmpty`, `isNotEmpty`, `isDefined`, `startsWith`, `endsWith`, and the other boolean functions all compose this way.
### Loops
```handlebars theme={null}
{{#each items}}
{{/each}}
```
`@index`, `@first`, `@last`, and `@key` (for objects) are available inside the block; `../` reaches the parent context. Each render carries a 5,000-iteration cap across all `#each` blocks — runaway loops fail loudly instead of hanging.
### Calling functions from Handlebars
Every function in the library below is available as a Handlebars helper using **positional arguments** (no parentheses or commas):
```handlebars theme={null}
{{titleCase title}}
{{currency price 'USD'}}
{{date publishedAt 'MMM D, YYYY'}}
{{truncate description 100 '...'}}
```
Partials (`{{> name}}`) are not supported — reuse happens at the template level.
## Expression syntax (canvas templates)
Canvas templates (`engine: "fabric"`) use the expression engine: function calls, arithmetic and comparison operators, and ternaries inside `{{expression}}`. The examples in the rest of this page use this style.
### Simple Variables
```
{{title}}
{{user.name}}
{{items[0].price}}
```
### Property Access
Access nested properties with dot notation:
```
{{user.profile.avatar}}
{{order.items[0].name}}
```
### Arithmetic
```
{{price * quantity}}
{{subtotal + tax}}
{{total / 100}}
{{count % 2}}
```
### Comparisons
```
{{price > 100}}
{{status == 'active'}}
{{count >= 10}}
{{name != 'Guest'}}
```
### Logical Operators
```
{{isAdmin && isPremium}}
{{hasDiscount || isFirstOrder}}
{{!isExpired}}
```
### Ternary Operator
```
{{isPremium ? 'Premium Member' : 'Free User'}}
{{count > 0 ? count : 'None'}}
```
## Built-in Functions
The function library is shared by both engines. Examples below use expression style (canvas engine); in Handlebars HTML templates call the same functions positionally — `{{currency price 'USD'}}` instead of `{{currency(price, 'USD')}}`.
### String Functions
| Function | Description | Example |
| ------------------------------- | ----------------------- | -------------------------------- |
| `uppercase(str)` | Convert to uppercase | `{{uppercase(name)}}` |
| `lowercase(str)` | Convert to lowercase | `{{lowercase(email)}}` |
| `capitalize(str)` | Capitalize first letter | `{{capitalize(title)}}` |
| `titleCase(str)` | Capitalize each word | `{{titleCase(name)}}` |
| `trim(str)` | Remove whitespace | `{{trim(input)}}` |
| `truncate(str, len, suffix)` | Truncate with suffix | `{{truncate(desc, 100, '...')}}` |
| `replace(str, search, replace)` | Replace all occurrences | `{{replace(text, '-', ' ')}}` |
| `split(str, delimiter)` | Split into array | `{{split(tags, ',')}}` |
| `padStart(str, len, char)` | Pad from start | `{{padStart(id, 5, '0')}}` |
| `padEnd(str, len, char)` | Pad from end | `{{padEnd(code, 10, '-')}}` |
### Number Functions
| Function | Description | Example |
| ---------------------- | ----------------- | -------------------------- |
| `round(num, decimals)` | Round to decimals | `{{round(price, 2)}}` |
| `floor(num)` | Round down | `{{floor(rating)}}` |
| `ceil(num)` | Round up | `{{ceil(shipping)}}` |
| `abs(num)` | Absolute value | `{{abs(difference)}}` |
| `min(a, b, ...)` | Minimum value | `{{min(price, maxPrice)}}` |
| `max(a, b, ...)` | Maximum value | `{{max(0, quantity)}}` |
| `sum(array)` | Sum array values | `{{sum(prices)}}` |
| `average(array)` | Average of array | `{{average(ratings)}}` |
### Formatting Functions
| Function | Description | Example |
| --------------------------------- | ------------------ | ------------------------------------ |
| `currency(num, currency, locale)` | Format as currency | `{{currency(price, 'USD')}}` |
| `number(num, locale)` | Format number | `{{number(count, 'en-US')}}` |
| `percent(num, decimals)` | Format as percent | `{{percent(rate, 1)}}` |
| `date(str, format)` | Format date | `{{date(createdAt, 'MMM D, YYYY')}}` |
| `time(str)` | Format time | `{{time(timestamp)}}` |
### Array Functions
| Function | Description | Example |
| ------------------------ | ----------------- | ------------------------------ |
| `length(arr)` | Array length | `{{length(items)}}` |
| `first(arr)` | First element | `{{first(images)}}` |
| `last(arr)` | Last element | `{{last(comments)}}` |
| `join(arr, separator)` | Join elements | `{{join(tags, ', ')}}` |
| `slice(arr, start, end)` | Slice array | `{{slice(items, 0, 3)}}` |
| `contains(arr, value)` | Check if contains | `{{contains(roles, 'admin')}}` |
| `indexOf(arr, value)` | Find index | `{{indexOf(items, 'apple')}}` |
### Type Checks
| Function | Description | Example |
| ----------------- | ------------------ | ---------------------- |
| `isEmpty(val)` | Check if empty | `{{isEmpty(items)}}` |
| `isNotEmpty(val)` | Check if not empty | `{{isNotEmpty(name)}}` |
| `isDefined(val)` | Check if defined | `{{isDefined(user)}}` |
| `isArray(val)` | Check if array | `{{isArray(items)}}` |
| `isString(val)` | Check if string | `{{isString(name)}}` |
| `isNumber(val)` | Check if number | `{{isNumber(count)}}` |
### Utilities
| Function | Description | Example |
| ------------------------ | ---------------- | -------------------------------------- |
| `default(val, fallback)` | Default if empty | `{{default(name, 'Guest')}}` |
| `coalesce(a, b, ...)` | First non-null | `{{coalesce(nickname, name, 'User')}}` |
| `json(val)` | To JSON string | `{{json(data)}}` |
| `parseJson(str)` | Parse JSON | `{{parseJson(jsonString)}}` |
## Conditional Rendering
### If/Else in Text
```
{{isPremium ? 'Premium Member' : 'Free User'}}
```
### Conditional Objects
In template objects, use the `_if` property:
```json theme={null}
{
"type": "textbox",
"text": "Premium Badge",
"_if": "isPremium"
}
```
The object is only rendered if the condition evaluates to true.
### Complex Conditions
```json theme={null}
{
"type": "image",
"src": "{{badge}}",
"_if": "isPremium && !isExpired"
}
```
## Text Interpolation
Combine static text with expressions:
```
Hello, {{name}}! You have {{count}} {{count == 1 ? 'message' : 'messages'}}.
```
Multiple expressions in one string:
```
{{currency(price, 'USD')}} ({{percent(discount)}} off)
```
## Testing Expressions
Test expressions before using them in templates:
### Validate Syntax
```bash theme={null}
curl -X POST https://api.pictify.io/templates/expression/validate \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"expression": "price * quantity"}'
```
Response:
```json theme={null}
{
"valid": true,
"expression": "price * quantity"
}
```
### Test with Data
```bash theme={null}
curl -X POST https://api.pictify.io/templates/expression/test \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expression": "currency(price * quantity, 'USD')",
"variables": {
"price": 29.99,
"quantity": 3
}
}'
```
Response:
```json theme={null}
{
"success": true,
"result": "$89.97",
"resultType": "string"
}
```
### List Available Functions
```bash theme={null}
curl https://api.pictify.io/templates/expression/functions \
-H "Authorization: Bearer $API_KEY"
```
## Examples
### Personalized Greeting
```
{{capitalize(greeting)}}, {{titleCase(name)}}!
```
### Price Display
```
{{currency(price, 'USD')}}{{hasDiscount ? ' (' + percent(discount) + ' off)' : ''}}
```
### Date Formatting
```
Published {{date(publishedAt, 'MMMM D, YYYY')}}
```
### Conditional Badge
```json theme={null}
{
"type": "rect",
"fill": "{{isPremium ? '#FFD700' : '#C0C0C0'}}",
"_if": "showBadge"
}
```
### Array Display
```
{{join(slice(tags, 0, 3), ' • ')}}{{length(tags) > 3 ? ' +' + (length(tags) - 3) + ' more' : ''}}
```
# Rendering
Source: https://docs.pictify.io/concepts/rendering
Output formats and rendering options
# Rendering
Pictify renders HTML/CSS templates to various output formats. This page covers rendering options, formats, and optimization.
## Output Formats
### Images
| Format | Extension | Best For | Transparency |
| ------ | --------- | --------------------------------------- | ------------ |
| PNG | `.png` | Screenshots, graphics with transparency | Yes |
| JPEG | `.jpg` | Photos, smaller file sizes | No |
| WebP | `.webp` | Modern browsers, best compression | Yes |
### Documents
| Format | Extension | Best For |
| ------ | --------- | ------------------------------------ |
| PDF | `.pdf` | Print, documents, multi-page content |
### Animated
| Format | Extension | Best For |
| ------ | --------- | ----------------------- |
| GIF | `.gif` | Animations, short loops |
## Rendering Options
### Dimensions
```typescript theme={null}
const image = await pictify.renderHtml({
html: '
Hello
',
width: 1200, // Output width in pixels (default: 1280)
height: 630, // Output height in pixels (default: 720)
});
```
Maximum dimensions: 4000x4000 pixels for authenticated users, 2000x2000 for public/trial.
### Format
`renderHtml` writes a PNG by default. Set `format` to choose the output type — it maps to the `/image` endpoint's `fileExtension`:
```typescript theme={null}
const image = await pictify.renderHtml({
html: '
Hello
',
format: 'jpeg' // 'png' | 'jpg' | 'jpeg' | 'webp' | 'pdf' (default: png)
});
```
The `/image` endpoint does not expose a `quality` knob. To control raster compression, render through a [template](/concepts/templates) (`pictify.render({ templateId, quality })`, where `quality` is `0.1`–`1.0`).
### Selector
Capture a specific element instead of the full page:
```typescript theme={null}
const image = await pictify.renderHtml({
html: '
...Target
',
selector: '#content' // Only capture this element
});
```
## Image Generation
### From HTML
```typescript theme={null}
const image = await pictify.renderHtml({
html: `
Hello World
`,
width: 1200,
height: 630
});
console.log(image.url);
```
### From URL
Capture a screenshot of any URL:
```typescript theme={null}
const screenshot = await pictify.renderUrl({
url: 'https://example.com/page',
width: 1200,
height: 630
});
console.log(screenshot.url);
```
### From Template
Render a saved template with variables:
```typescript theme={null}
const result = await pictify.render({
templateId: 'template-id',
variables: {
title: 'Dynamic Content'
}
});
// Access the rendered image (result.url is a convenience accessor for results[0].url)
console.log(result.url);
console.log(result.results[0].url);
```
### Layout Variants
Templates can have multiple layout variants for different platforms (e.g., Twitter, Facebook, Instagram). Render a specific layout or multiple layouts at once:
```typescript theme={null}
// Render a specific layout
const single = await pictify.render({
templateId: 'template-id',
variables: { title: 'Hello' },
layout: 'twitter-post'
});
// Render multiple layouts in one request (max 20)
const result = await pictify.renderLayouts({
templateId: 'template-id',
variables: { title: 'Hello' },
layouts: ['default', 'twitter-post', 'facebook-post']
});
// Each layout is a separate result
result.results.forEach(r => {
console.log(`${r.name} (${r.width}x${r.height}): ${r.url}`);
});
```
The `default` layout is the base template. Use the layout key (e.g., `twitter-post`, `facebook-post`) to render a specific variant. Layout keys are set when creating variants via the AI Resize feature in the editor.
### From Canvas Data
Render FabricJS canvas data directly via the `POST /image/canvas` REST endpoint. The SDKs don't wrap this endpoint, so call it with your HTTP client of choice:
```bash theme={null}
curl -X POST https://api.pictify.io/image/canvas \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"engine": "fabric",
"fabricJSData": {
"version": "5.3.0",
"objects": [
{ "type": "rect", "fill": "#667eea", "width": 1200, "height": 630 },
{ "type": "textbox", "text": "Hello", "fill": "white", "top": 100, "left": 100 }
]
},
"variables": { "greeting": "Hello World" },
"width": 1200,
"height": 630
}'
```
See the [Create Canvas Image](/api-reference/endpoints/images/canvas) reference for the full request schema.
## GIF Generation
### From HTML Animation
Capture CSS animations:
```typescript theme={null}
const gif = await pictify.renderGif({
html: `
Animated Text
`,
width: 600,
height: 400
});
console.log(gif.url, gif.animationLength);
```
### From a Live URL
Record motion on a live page:
```typescript theme={null}
const gif = await pictify.renderGif({
url: 'https://example.com/animated-page',
width: 800,
height: 600,
quality: 'high' // 'low' | 'medium' | 'high' (default: medium)
});
```
### Quality Presets
| Preset | Frame Rate | Max Duration | File Size |
| -------- | ---------- | ------------ | --------- |
| `low` | 10 fps | 10s | Small |
| `medium` | 15 fps | 15s | Medium |
| `high` | 24 fps | 30s | Large |
## PDF Generation
### From a Template (SDK)
The quickest way to produce a PDF is to render a template with `format: 'pdf'`:
```typescript theme={null}
const result = await pictify.render({
templateId: 'invoice-template',
variables: {
invoiceNumber: 'INV-001',
},
format: 'pdf'
});
console.log(result.url);
```
### Paper Sizes & Multi-Page (REST)
The dedicated PDF endpoints (`POST /pdf/render` and `POST /pdf/multi-page`) accept paper-size presets, margins, and per-page content. The SDKs don't wrap these directly, so call them with your HTTP client:
```bash theme={null}
# Single page with a paper-size preset and margins
curl -X POST https://api.pictify.io/pdf/render \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateUid": "invoice-template",
"variables": { "invoiceNumber": "INV-001" },
"preset": "A4",
"margins": { "top": 20, "bottom": 20, "left": 20, "right": 20 }
}'
```
```bash theme={null}
# Multi-page: one page per variable set
curl -X POST https://api.pictify.io/pdf/multi-page \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateUid": "report-template",
"variableSets": [
{ "pageTitle": "Introduction", "content": "..." },
{ "pageTitle": "Chapter 1", "content": "..." },
{ "pageTitle": "Conclusion", "content": "..." }
],
"preset": "A4"
}'
```
See the [PDF Generation](/api-reference/generation/pdfs) reference for all options.
### PDF Presets
| Preset | Dimensions | Use Case |
| -------- | ------------ | ------------------ |
| `A4` | 210 × 297 mm | Standard documents |
| `Letter` | 8.5 × 11 in | US letter size |
| `Legal` | 8.5 × 14 in | Legal documents |
| `A3` | 297 × 420 mm | Larger documents |
| `A5` | 148 × 210 mm | Booklets |
| `custom` | User-defined | Custom sizes |
```bash theme={null}
# Custom PDF size
curl -X POST https://api.pictify.io/pdf/render \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateUid": "template-id",
"preset": "custom",
"customSize": { "width": 800, "height": 600 }
}'
```
## Screenshot a Section of a Page
To capture a specific region of a live page, screenshot the URL and crop to an element with `selector`:
```typescript theme={null}
const screenshot = await pictify.renderUrl({
url: 'https://stripe.com/pricing',
selector: '#pricing', // crop to this element
width: 1200,
height: 630
});
console.log(screenshot.url);
```
Prefer natural language? The [Pictify MCP server](/agent-integration/mcp-server) lets AI agents request screenshots conversationally — the SDKs themselves expose the direct `renderUrl` call shown above.
## Response Format
The template render endpoint always returns a `results` array, even for single-layout renders:
```json theme={null}
{
"results": [
{
"layout": "default",
"name": "Default",
"url": "https://cdn.pictify.io/renders/abc123.png",
"width": 1200,
"height": 630,
"format": "png",
"id": "abc123"
}
],
"errors": [],
"totalLayouts": 1,
"totalRendered": 1,
"totalErrors": 0,
"templateUid": "TEMPLATE_UID"
}
```
Multi-layout response:
```json theme={null}
{
"results": [
{
"layout": "default",
"name": "Default",
"url": "https://cdn.pictify.io/renders/abc123.png",
"width": 1080,
"height": 1080,
"format": "png"
},
{
"layout": "twitter-post",
"name": "Twitter/X Post",
"url": "https://cdn.pictify.io/renders/def456.png",
"width": 1200,
"height": 675,
"format": "png"
}
],
"errors": [],
"totalLayouts": 2,
"totalRendered": 2,
"totalErrors": 0,
"templateUid": "TEMPLATE_UID"
}
```
Other render endpoints (HTML, URL, GIF) return:
```json theme={null}
{
"url": "https://cdn.pictify.io/renders/abc123.png",
"id": "abc123",
"width": 1200,
"height": 630,
"createdAt": "2026-01-29T10:30:00Z"
}
```
## User Storage
If you've configured user storage (S3, R2, etc.), renders are also uploaded to your storage:
```json theme={null}
{
"url": "https://cdn.pictify.io/renders/abc123.png",
"userStorageUrl": "https://your-bucket.s3.amazonaws.com/renders/abc123.png"
}
```
## Batch Rendering
Render many variable sets from a single template. Batch rendering is **asynchronous**: submitting returns immediately with a `batchId`, and the job runs in the background.
### Request
```typescript theme={null}
const job = await pictify.renderBatch({
templateId: 'product-card',
variableSets: [
{ title: 'Card 1' },
{ title: 'Card 2' },
], // max 100 per batch
layouts: ['default', 'twitter-post'], // optional; or `layout: 'square'`
});
// { batchId, status, totalItems }
console.log(job.batchId);
```
Pass `layout` (string) for a single variant, or `layouts` (array) for multiple variants per item.
### Tracking Progress
Poll `getBatchResults(batchId)` to track status. The poll response reports per-item `index`, `success`, and `variables` — but **not** rendered URLs:
```json theme={null}
{
"batchId": "batch_abc123",
"status": "completed",
"progress": 100,
"totalItems": 2,
"completedItems": 2,
"failedItems": 0,
"results": [
{ "index": 0, "success": true, "variables": ["title"] },
{ "index": 1, "success": true, "variables": ["title"] }
],
"errors": []
}
```
```typescript theme={null}
const status = await pictify.getBatchResults(job.batchId);
console.log(status.status, status.completedItems, 'of', status.totalItems);
for (const item of status.results) {
console.log(`item ${item.index}: success=${item.success}`);
}
```
**Rendered URLs are not returned by the poll endpoint.** Final image URLs are delivered via the `render.completed` webhook — subscribe to [webhooks](/concepts/webhooks) to collect batch output. See the [Batch Processing guide](/guides/batch-processing) for the full workflow.
## Performance Tips
1. **Use appropriate dimensions** - Don't render larger than needed
2. **Choose the right format** - JPEG for photos, PNG for graphics, WebP for best compression
3. **Tune template quality** - for template renders, `quality` 0.9 is usually indistinguishable from 1.0
4. **Batch similar renders** - Use batch rendering for many images from one template
5. **Cache rendered images** - Store URLs and reuse instead of re-rendering
# Templates
Source: https://docs.pictify.io/concepts/templates
Create reusable designs with dynamic variables
# Templates
Templates are reusable designs with dynamic content areas called variables. Create a template once, then render it with different data to generate unique images.
## Creating Templates
### Visual Editor
The dashboard provides a drag-and-drop editor for creating templates:
1. Go to **Templates** in the dashboard
2. Click **Create Template**
3. Use the canvas to add text, images, shapes, and backgrounds
4. Mark elements as variables by clicking the variable icon
5. Save your template
### HTML/CSS Import
You can also create templates from HTML/CSS:
```html theme={null}
{{title}}
{{description}}
{{authorName}}
```
## Variables
Variables are placeholders that get replaced with actual values when rendering. Define variables using double curly braces: `{{variableName}}`.
### Variable Types
| Type | Description | Example |
| --------- | ------------------ | -------------------------------------- |
| `text` | Plain text content | `{{title}}`, `{{description}}` |
| `image` | Image URL | `{{logo}}`, `{{avatar}}` |
| `color` | Hex or RGB color | `{{backgroundColor}}`, `{{textColor}}` |
| `number` | Numeric value | `{{price}}`, `{{count}}` |
| `boolean` | True/false value | `{{showBadge}}`, `{{isPremium}}` |
### Variable Definitions
Each variable can have a definition that includes:
```json theme={null}
{
"name": "title",
"type": "text",
"defaultValue": "Untitled",
"description": "The main headline",
"validation": {
"required": true,
"maxLength": 100
}
}
```
### Getting Template Variables
Retrieve variable definitions before rendering:
```typescript Node.js theme={null}
const template = await pictify.getTemplate('template-id');
console.log(template.variableDefinitions);
// [
// { name: 'title', type: 'text', required: true },
// { name: 'backgroundColor', type: 'color', defaultValue: '#ffffff' }
// ]
```
```python Python theme={null}
template = client.get_template("template-id")
for var in (template.variable_definitions or []):
print(f"{var.name}: {var.type} (default: {var.default_value})")
```
## Rendering Templates
Render a template by providing values for its variables:
```typescript Node.js theme={null}
const result = await pictify.render({
templateId: 'og-template',
variables: {
title: 'How to Build Great Products',
description: 'A guide to product development',
authorName: 'Jane Doe',
authorImage: 'https://example.com/jane.jpg'
}
});
console.log(result.url);
```
```bash cURL theme={null}
curl -X POST https://api.pictify.io/templates/og-template/render \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"variables": {
"title": "How to Build Great Products",
"description": "A guide to product development",
"authorName": "Jane Doe",
"authorImage": "https://example.com/jane.jpg"
}
}'
```
## Template Structure
Templates are stored as FabricJS canvas data with additional metadata:
```json theme={null}
{
"uid": "tmpl_abc123",
"name": "OG Image Template",
"width": 1200,
"height": 630,
"outputFormat": "image",
"variableDefinitions": [
{
"name": "title",
"type": "text",
"defaultValue": "Untitled"
}
],
"fabricJSData": {
"version": "5.3.0",
"objects": [
{
"type": "textbox",
"text": "{{title}}",
"isVariable": true,
"variableBindings": [
{ "variableName": "title", "property": "text" }
]
}
]
}
}
```
## Output Formats
Templates support multiple output formats:
| Format | Extension | Use Case |
| ------ | --------- | ------------------------------------ |
| PNG | `.png` | Transparent backgrounds, screenshots |
| JPEG | `.jpg` | Photos, smaller file sizes |
| WebP | `.webp` | Modern browsers, best compression |
| PDF | `.pdf` | Documents, print materials |
| GIF | `.gif` | Animated content |
Set the output format when rendering:
```typescript theme={null}
const result = await pictify.render({
templateId: 'template-id',
variables: { title: 'Hello' },
format: 'pdf'
});
```
## Multi-Page Templates
PDF templates can have multiple pages — one page per variable set. This is served by the `POST /pdf/multi-page` REST endpoint, which the SDKs don't wrap directly:
```bash theme={null}
curl -X POST https://api.pictify.io/pdf/multi-page \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateUid": "invoice-template",
"variableSets": [
{ "pageTitle": "Page 1" },
{ "pageTitle": "Page 2" }
]
}'
```
See the [PDF Generation](/api-reference/generation/pdfs) reference for paper sizes, margins, and orientation options.
## Listing Templates
List your templates with pagination. `listTemplates` returns `{ templates, pagination }`:
```typescript theme={null}
const { templates, pagination } = await pictify.listTemplates({
page: 1, // optional (default: 1)
limit: 20, // optional, max 100 (default: 12)
sort: 'newest', // optional: 'newest' | 'oldest' | 'name'
});
console.log(pagination.total, 'templates');
for (const t of templates) console.log(t.uid, t.name);
```
## Best Practices
1. **Use descriptive variable names**: `authorProfileImage` instead of `img1`
2. **Set default values**: Ensure templates render even with missing data
3. **Validate input**: Use validation rules to catch errors early
4. **Keep templates focused**: One template per use case
5. **Version templates**: Use naming conventions like `og-image-v2`
# Webhooks
Source: https://docs.pictify.io/concepts/webhooks
Receive real-time notifications for render events
# Webhooks
Webhooks allow you to receive real-time HTTP notifications when events occur in your Pictify account. Use them to trigger workflows, update databases, or integrate with third-party services.
## Supported Events
| Event | Description |
| ------------------ | ----------------------------------------------- |
| `render.completed` | Image, GIF, or PDF render finished successfully |
| `render.failed` | Render failed with an error |
| `binding.updated` | Binding data refreshed |
| `binding.failed` | Binding data fetch failed |
## Creating a Webhook
### Dashboard
1. Go to **Settings** > **Webhooks**
2. Click **Create Webhook**
3. Select the event type
4. Enter your endpoint URL
5. Save and copy the signing secret
### API
```bash theme={null}
curl -X POST https://api.pictify.io/webhook-subscriptions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event": "render.completed",
"targetUrl": "https://your-server.com/webhooks/pictify",
"platform": "custom"
}'
```
Response includes the signing secret (only shown once):
```json theme={null}
{
"subscription": {
"uid": "wh_abc123",
"event": "render.completed",
"targetUrl": "https://your-server.com/webhooks/pictify",
"status": "active",
"secret": "whsec_xyz789..."
}
}
```
## Webhook Payload
All webhooks include these headers:
```http theme={null}
Content-Type: application/json
X-Pictify-Signature: t=1706515260,v1=abc123...
X-Pictify-Event: render.completed
X-Pictify-Delivery-Id: del_xyz789
```
### render.completed
```json theme={null}
{
"event": "render.completed",
"timestamp": "2026-01-29T10:30:00Z",
"data": {
"type": "image",
"source": "api",
"imageId": "img_abc123",
"url": "https://cdn.pictify.io/renders/abc123.png",
"userStorageUrl": "https://your-bucket.s3.amazonaws.com/abc123.png",
"width": 1200,
"height": 630,
"templateId": "tmpl_xyz789",
"userId": "user_123",
"renderedAt": "2026-01-29T10:30:00Z"
}
}
```
### render.failed
```json theme={null}
{
"event": "render.failed",
"timestamp": "2026-01-29T10:30:00Z",
"data": {
"type": "image",
"templateId": "tmpl_xyz789",
"error": "Template not found",
"errorCode": "TEMPLATE_NOT_FOUND"
}
}
```
## Signature Verification
Always verify webhook signatures to ensure requests are from Pictify and haven't been tampered with.
The signature header format is:
```
X-Pictify-Signature: t=1706515260,v1=abc123...
```
Where:
* `t` = Unix timestamp when the webhook was sent
* `v1` = HMAC-SHA256 signature of `{timestamp}.{payload}`
### Verification Steps
1. Extract timestamp and signature from header
2. Reject if timestamp is older than 5 minutes (replay protection)
3. Compute expected signature: `HMAC-SHA256(secret, "{timestamp}.{payload}")`
4. Compare signatures using constant-time comparison
### Code Examples
```typescript Node.js theme={null}
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signatureHeader: string,
secret: string
): boolean {
const parts: Record = {};
for (const pair of signatureHeader.split(',')) {
const [key, value] = pair.split('=');
parts[key] = value;
}
const timestamp = parseInt(parts.t, 10);
const providedSignature = parts.v1;
// Reject if timestamp is older than 5 minutes
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
throw new Error('Webhook timestamp too old');
}
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(providedSignature),
Buffer.from(expectedSignature)
);
}
// Express.js example
app.post('/webhooks/pictify', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-pictify-signature'];
const payload = req.body.toString();
try {
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
console.log('Received event:', event.event);
res.status(200).send('OK');
} catch (error) {
res.status(400).send(error.message);
}
});
```
```python Python theme={null}
import hmac
import hashlib
import time
def verify_webhook_signature(payload: bytes, signature_header: str, secret: str) -> bool:
parts = dict(pair.split('=') for pair in signature_header.split(','))
timestamp = int(parts['t'])
provided_signature = parts['v1']
# Reject if timestamp is older than 5 minutes
if abs(time.time() - timestamp) > 300:
raise ValueError("Webhook timestamp too old")
signed_payload = f"{timestamp}.{payload.decode()}"
expected_signature = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(provided_signature, expected_signature)
# Flask example
@app.route('/webhooks/pictify', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Pictify-Signature')
payload = request.get_data()
try:
if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.get_json()
print(f"Received event: {event['event']}")
return 'OK', 200
except ValueError as e:
return str(e), 400
```
```go Go theme={null}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"math"
"strconv"
"strings"
"time"
)
func verifyWebhookSignature(payload, signatureHeader, secret string) error {
parts := make(map[string]string)
for _, pair := range strings.Split(signatureHeader, ",") {
kv := strings.SplitN(pair, "=", 2)
if len(kv) == 2 {
parts[kv[0]] = kv[1]
}
}
timestamp, _ := strconv.ParseInt(parts["t"], 10, 64)
providedSignature := parts["v1"]
// Reject if timestamp is older than 5 minutes
if math.Abs(float64(time.Now().Unix()-timestamp)) > 300 {
return fmt.Errorf("webhook timestamp too old")
}
signedPayload := fmt.Sprintf("%d.%s", timestamp, payload)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
expectedSignature := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(providedSignature), []byte(expectedSignature)) {
return fmt.Errorf("invalid signature")
}
return nil
}
```
```ruby Ruby theme={null}
require 'openssl'
def verify_webhook_signature(payload, signature_header, secret)
parts = signature_header.split(',').map { |p| p.split('=') }.to_h
timestamp = parts['t'].to_i
provided_signature = parts['v1']
# Reject if timestamp is older than 5 minutes
if (Time.now.to_i - timestamp).abs > 300
raise 'Webhook timestamp too old'
end
signed_payload = "#{timestamp}.#{payload}"
expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret, signed_payload)
Rack::Utils.secure_compare(provided_signature, expected_signature)
end
# Sinatra example
post '/webhooks/pictify' do
signature = request.env['HTTP_X_PICTIFY_SIGNATURE']
payload = request.body.read
begin
unless verify_webhook_signature(payload, signature, ENV['WEBHOOK_SECRET'])
halt 401, 'Invalid signature'
end
event = JSON.parse(payload)
puts "Received event: #{event['event']}"
status 200
'OK'
rescue => e
halt 400, e.message
end
end
```
## Managing Webhooks
### List Webhooks
```bash theme={null}
curl https://api.pictify.io/webhook-subscriptions \
-H "Authorization: Bearer $API_KEY"
```
### Pause / Resume a Webhook
Pause and resume are currently available via the dashboard only. API support is coming soon.
### Delete a Webhook
```bash theme={null}
curl -X DELETE https://api.pictify.io/webhook-subscriptions/{uid} \
-H "Authorization: Bearer $API_KEY"
```
## Filters
Filter webhooks to receive only specific events:
```bash theme={null}
curl -X POST https://api.pictify.io/webhook-subscriptions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event": "render.completed",
"targetUrl": "https://your-server.com/webhooks",
"filters": {
"templateId": "tmpl_abc123"
}
}'
```
## Delivery & Retries
* Webhooks are delivered within seconds of events
* Failed deliveries retry with exponential backoff: 1min, 5min, 30min, 2hr, 24hr
* After 5 failed attempts, the webhook is paused
* Check delivery status in the dashboard or via API
## Best Practices
1. **Always verify signatures** - Protect against spoofed requests
2. **Respond quickly** - Return 2xx within 30 seconds, process async
3. **Handle duplicates** - Use delivery ID for idempotency
4. **Monitor failures** - Set up alerts for webhook delivery issues
5. **Use HTTPS** - Never use HTTP endpoints in production
# API Key Management
Source: https://docs.pictify.io/dashboard/api-keys
Create and manage API keys in the dashboard
# API Key Management
API keys authenticate your applications with the Pictify API. This guide covers creating, managing, and securing your keys.
## Creating an API Key
1. Go to **Settings** > **API Keys**
2. Click **Create Key**
3. Enter a descriptive name (e.g., "Production Server", "CI/CD Pipeline")
4. Click **Create**
5. **Copy the key immediately** - it's only shown once
Store your API key securely. You won't be able to see it again after leaving this page.
## Key Types
### Live Keys
* Full access; every render consumes plan credits
* Full API access
* Usage counts against your plan
* Use in production
### Test Keys
* (There is no separate test key type — use free-tier credits to experiment)
* Use for development and testing
## Viewing Keys
The API Keys page shows:
| Column | Description |
| ------------- | -------------------------------------- |
| **Name** | Your key description |
| **Key ID** | Public identifier (e.g., `key_abc123`) |
| **Type** | Live or Test |
| **Created** | Creation date |
| **Last Used** | Most recent API call |
| **Status** | Active or Revoked |
For security, only the Key ID is displayed. The full key value is only shown at creation.
## Managing Keys
### Rename a Key
1. Click the **...** menu on a key
2. Select **Rename**
3. Enter the new name
4. Click **Save**
### Revoke a Key
Revoking a key immediately invalidates it:
1. Click the **...** menu on a key
2. Select **Revoke**
3. Confirm the action
Revoking a key is immediate and permanent. Any applications using this key will stop working.
### Delete a Key
Remove a key from your account:
1. Click the **...** menu on a key
2. Select **Delete**
3. Confirm deletion
Only revoked keys can be deleted.
## Key Limits
| Plan | Live Keys | Test Keys |
| ---------- | --------- | --------- |
| Free | 2 | 5 |
| Pro | 10 | 10 |
| Business | 50 | 50 |
| Enterprise | Unlimited | Unlimited |
## Usage Tracking
### Per-Key Usage
View usage for each key:
1. Click a key to expand details
2. See requests in the last 24h, 7d, 30d
3. View error rates and latency
### Usage Alerts
Set up alerts for unusual activity:
1. Go to **Settings** > **Alerts**
2. Click **Add Alert**
3. Configure conditions:
* Requests exceed threshold
* Error rate above percentage
* Latency above threshold
4. Choose notification method (email, Slack, webhook)
## Security Best Practices
### Use Descriptive Names
Name keys by their purpose:
```
✅ "Production API Server"
✅ "Staging Environment"
✅ "GitHub Actions CI"
❌ "Key 1"
❌ "Test"
```
### Rotate Keys Regularly
Schedule regular key rotation:
1. Create a new key
2. Update your application
3. Verify the new key works
4. Revoke the old key
### Use Test Keys for Development
Never use live keys in development:
```bash theme={null}
# Development
PICTIFY_API_KEY=your-api-key
# Production
PICTIFY_API_KEY=your-api-key
```
### Monitor for Misuse
Watch for signs of compromised keys:
* Unexpected usage spikes
* Requests from unknown IPs
* Unusual error patterns
### Principle of Least Privilege
Create separate keys for different services:
| Service | Key | Access |
| ---------- | ---------------- | ----------- |
| Web App | `prod-webapp` | Full access |
| Mobile App | `prod-mobile` | Full access |
| Analytics | `prod-analytics` | Read-only |
| CI/CD | `ci-pipeline` | Test key |
## Environment-Specific Keys
### Development
Use test keys with a local `.env`:
```bash theme={null}
# .env.local
PICTIFY_API_KEY=YOUR_DEVELOPMENT_KEY
```
### Staging
Use test keys for staging environments:
```bash theme={null}
# staging.env
PICTIFY_API_KEY=YOUR_STAGING_KEY
```
### Production
Use live keys, stored securely:
```bash theme={null}
# Set via secrets manager, not in files
PICTIFY_API_KEY=YOUR_PRODUCTION_KEY
```
## Troubleshooting
### "Invalid API Key" Error
1. Verify the key is correct (no extra spaces)
2. Check the key hasn't been revoked
3. Ensure you're using the right key type (live vs test)
### Key Not Working After Creation
1. Wait a few seconds - propagation takes up to 30 seconds
2. Verify you copied the full key
3. Check for encoding issues if copying from another source
### Usage Not Updating
Usage statistics may have up to 5 minutes delay. Real-time usage is available in the API response headers.
## API Key API
Manage keys programmatically with the [API Keys API](/authentication):
```bash theme={null}
# List keys
curl https://api.pictify.io/api-keys \
-H "Authorization: Bearer $ADMIN_KEY"
# Create key
curl -X POST https://api.pictify.io/api-keys \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "New Production Key", "type": "live"}'
```
API key management requires an admin-level key with key management permissions.
# Managing Templates
Source: https://docs.pictify.io/dashboard/templates
Create and manage templates in the Pictify dashboard
# Managing Templates
The Pictify dashboard provides a visual editor for creating and managing templates. This guide covers the template workflow from creation to rendering.
## Template Editor
### Accessing the Editor
1. Go to **Templates** in the sidebar
2. Click **Create Template** or select an existing template
3. The visual editor opens with the canvas and toolbar
### Editor Layout
| Area | Description |
| -------------------- | ------------------------------- |
| **Canvas** | Visual preview of your template |
| **Toolbar** | Add elements, adjust settings |
| **Properties Panel** | Configure selected element |
| **Variables Panel** | Define and manage variables |
| **Layers Panel** | Reorder and group elements |
## Creating a Template
### Step 1: Set Canvas Size
Choose a preset or enter custom dimensions:
| Preset | Dimensions | Use Case |
| --------------------- | ----------- | ----------------------------------- |
| **OG Image** | 1200 × 630 | Social sharing (Facebook, LinkedIn) |
| **Twitter Card** | 1200 × 600 | Twitter posts |
| **Instagram Post** | 1080 × 1080 | Instagram feed |
| **Instagram Story** | 1080 × 1920 | Instagram/Facebook stories |
| **YouTube Thumbnail** | 1280 × 720 | Video thumbnails |
| **Custom** | Any | Your specific needs |
### Step 2: Add Elements
Click the **+** button to add elements:
* **Text** - Headlines, body text, captions
* **Image** - Logos, photos, icons
* **Shape** - Rectangles, circles, custom shapes
* **Container** - Group elements together
### Step 3: Style Elements
Select an element to edit its properties:
**Text Properties:**
* Font family, size, weight
* Color and opacity
* Alignment and line height
* Letter spacing
**Image Properties:**
* Source URL or upload
* Fit mode (cover, contain, fill)
* Border radius
* Filters and effects
**Shape Properties:**
* Fill color or gradient
* Border width and color
* Shadow effects
* Opacity
### Step 4: Add Variables
Variables make your template dynamic. Click **Variables** to add:
1. Click **Add Variable**
2. Enter a name (e.g., `title`, `author`)
3. Set the type (string, number, boolean, image)
4. Optionally set a default value
Use variables in text with double braces:
```
{{title}}
{{author.name}}
{{currency(price, 'USD')}}
```
### Step 5: Save Template
1. Click **Save**
2. Enter a name and description
3. Click **Create Template**
Your template is now available via API.
## Template Settings
### General Settings
| Setting | Description |
| --------------- | ---------------------------------------------- |
| **Name** | Template identifier in the dashboard |
| **Description** | Notes about the template's purpose |
| **Tags** | Categorize templates (e.g., "social", "email") |
### Render Settings
| Setting | Description |
| ------------------ | --------------------------- |
| **Default Format** | PNG, JPEG, or WebP |
| **Quality** | Compression level (1-100) |
| **Device Scale** | Retina support (1x, 2x, 3x) |
### Access Settings
| Setting | Description |
| ------------------- | --------------------------- |
| **Visibility** | Public URL or API-only |
| **Allowed Origins** | CORS origins for public URL |
## Working with Variables
### Variable Types
| Type | Description | Example |
| ----------- | -------------- | ----------------- |
| **String** | Text content | `"Hello World"` |
| **Number** | Numeric values | `42`, `3.14` |
| **Boolean** | True/false | `true`, `false` |
| **Image** | Image URL | `"https://..."` |
| **Array** | List of items | `["a", "b", "c"]` |
| **Object** | Nested data | `{name: "..."}` |
### Using Variables in Text
```
Hello, {{name}}!
You have {{count}} new messages.
Total: {{currency(price, 'USD')}}
```
### Conditional Elements
Make elements appear based on conditions:
1. Select an element
2. In Properties, find **Conditional**
3. Enter a condition: `isPremium` or `count > 0`
The element only renders when the condition is true.
### Default Values
Set fallback values for variables:
1. Click the variable in the Variables panel
2. Enter a **Default Value**
3. This value is used when the variable isn't provided
## Organizing Templates
### Folders
Create folders to organize templates:
1. Click **New Folder** in the Templates view
2. Name your folder
3. Drag templates into folders
### Tags
Add tags for filtering:
1. Open template settings
2. Add tags in the **Tags** field
3. Filter by tags in the Templates list
### Search
Use the search bar to find templates by:
* Name
* Description
* Tags
* Variable names
## Duplicating Templates
Create a copy to modify:
1. Click the **...** menu on a template
2. Select **Duplicate**
3. Edit the copy as needed
## Deleting Templates
Deleting a template is permanent. Existing images won't be affected, but you won't be able to render new ones.
1. Click the **...** menu on a template
2. Select **Delete**
3. Confirm deletion
## Version History
View and restore previous versions:
1. Open a template
2. Click **History** in the toolbar
3. Browse previous versions
4. Click **Restore** to revert
## Previewing Templates
### In-Editor Preview
1. Click **Preview** in the toolbar
2. Enter sample variable values
3. See the rendered result
### Test Renders
Generate a test image:
1. Click **Test Render**
2. Fill in variables
3. Click **Generate**
4. View or download the result
Test renders don't count against your quota.
## Exporting Templates
### Export as JSON
Export template configuration:
1. Click **...** menu
2. Select **Export**
3. Download the JSON file
### Import Template
Import from JSON:
1. Click **Import** in the Templates view
2. Select your JSON file
3. Review and save
## Keyboard Shortcuts
| Shortcut | Action |
| ---------------------- | ----------------- |
| `Cmd/Ctrl + S` | Save template |
| `Cmd/Ctrl + Z` | Undo |
| `Cmd/Ctrl + Shift + Z` | Redo |
| `Cmd/Ctrl + D` | Duplicate element |
| `Delete` | Delete selected |
| `Cmd/Ctrl + G` | Group elements |
| `Arrow keys` | Nudge element |
| `Shift + Arrow` | Nudge 10px |
## Best Practices
### Design Tips
1. **Use a grid** - Enable snap-to-grid for alignment
2. **Set up styles** - Create reusable colors and fonts
3. **Group related elements** - Keep layers organized
4. **Test at different sizes** - Preview on various devices
### Variable Naming
```
✅ title, authorName, publishDate
❌ t, var1, x
```
Use descriptive, camelCase names.
### Performance
1. **Optimize images** - Use appropriate sizes
2. **Limit fonts** - Load only needed weights
3. **Simplify gradients** - Complex gradients slow rendering
4. **Test render time** - Keep under 5 seconds
# Usage & Billing
Source: https://docs.pictify.io/dashboard/usage
Monitor usage and manage your Pictify subscription
# Usage & Billing
Track your API usage, understand your billing, and manage your Pictify subscription.
## Usage Dashboard
Access usage analytics at **Settings** > **Usage**.
### Overview Metrics
| Metric | Description |
| ----------------- | ------------------------------------- |
| **Total Renders** | Images, GIFs, and PDFs generated |
| **API Calls** | Total API requests (including failed) |
| **Bandwidth** | Data transferred from CDN |
| **Storage** | Template and asset storage used |
### Time Periods
View usage for different periods:
* **Today** - Current day (UTC)
* **This Week** - Sunday to Saturday
* **This Month** - Calendar month
* **Custom Range** - Select specific dates
### Charts
Interactive charts show:
* **Daily renders** over time
* **Renders by type** (image, GIF, PDF)
* **Renders by template**
* **API response times**
* **Error rates**
## Plan Limits
### Free Plan
| Resource | Limit |
| --------- | ----------- |
| Renders | 100/month |
| API Calls | 1,000/month |
| Templates | 5 |
| Bandwidth | 1 GB/month |
### Pro Plan
| Resource | Limit |
| --------- | ------------ |
| Renders | 5,000/month |
| API Calls | 50,000/month |
| Templates | 50 |
| Bandwidth | 50 GB/month |
### Business Plan
| Resource | Limit |
| --------- | ------------- |
| Renders | 50,000/month |
| API Calls | 500,000/month |
| Templates | Unlimited |
| Bandwidth | 500 GB/month |
### Enterprise
Custom limits based on your needs. Contact sales for details.
## Usage Alerts
Get notified before reaching limits:
1. Go to **Settings** > **Alerts**
2. Configure thresholds:
* 50%, 75%, 90%, 100% of limit
3. Choose notification method:
* Email
* Slack
* Webhook
## Billing
### Viewing Invoices
1. Go to **Settings** > **Billing**
2. Click **Invoices** tab
3. Download PDF invoices
### Payment Methods
Add or update payment methods:
1. Go to **Settings** > **Billing**
2. Click **Payment Methods**
3. Add card or connect bank account
### Billing Cycle
* Monthly plans bill on subscription date
* Annual plans bill once per year
* Overages are billed at the end of each month
## Upgrading Your Plan
### From Dashboard
1. Go to **Settings** > **Billing**
2. Click **Change Plan**
3. Select your new plan
4. Confirm upgrade
Upgrades take effect immediately. You're prorated for the remaining billing period.
### Downgrading
1. Go to **Settings** > **Billing**
2. Click **Change Plan**
3. Select lower tier
Downgrades take effect at the end of your billing period.
## Overage Handling
When you exceed plan limits:
### Soft Limits (Pro and above)
* API continues to work
* Overage charges apply
* Rates vary by resource
### Hard Limits (Free plan)
* API returns `429 Plan Limit Exceeded`
* Upgrade or wait for next billing cycle
### Overage Rates
| Resource | Pro Overage | Business Overage |
| --------- | ------------ | ---------------- |
| Renders | \$0.01/each | \$0.005/each |
| API Calls | \$0.001/each | \$0.0005/each |
| Bandwidth | \$0.10/GB | \$0.05/GB |
## Cost Optimization
### Reduce Renders
1. **Cache rendered images** - Don't regenerate unchanged content
2. **Use bindings** - Auto-refresh images instead of re-rendering
3. **Batch similar renders** - Use batch API for efficiency
### Reduce Bandwidth
1. **Choose appropriate formats** - JPEG for photos, PNG for graphics
2. **Optimize dimensions** - Don't render larger than needed
3. **Use CDN caching** - Set appropriate cache headers
### Right-Size Your Plan
Compare your usage to plan limits:
1. View historical usage in dashboard
2. Identify usage patterns
3. Choose plan that fits typical usage
4. Consider annual plans for savings
## Team Usage
For team accounts, view usage by:
### By Team Member
See renders attributed to each user:
1. Go to **Usage** > **By User**
2. Filter by date range
3. View per-user metrics
### By API Key
Track which keys generate the most usage:
1. Go to **Usage** > **By API Key**
2. Identify high-usage keys
3. Set per-key rate limits if needed
## Export Usage Data
Download usage data for analysis:
1. Go to **Settings** > **Usage**
2. Click **Export**
3. Choose format (CSV, JSON)
4. Select date range
5. Download file
## Understanding Your Invoice
Invoice line items include:
| Item | Description |
| --------------------- | ----------------------------------------- |
| **Base Plan** | Monthly/annual subscription |
| **Render Overage** | Additional renders beyond plan |
| **API Call Overage** | Additional API calls beyond plan |
| **Bandwidth Overage** | Additional data transfer |
| **Add-ons** | Premium features (priority support, etc.) |
| **Credits** | Applied promotional credits |
## Tax Information
### VAT/GST
If you're in a region with VAT/GST:
1. Go to **Settings** > **Billing**
2. Click **Tax Information**
3. Enter your VAT/GST number
4. Tax will be adjusted on future invoices
### Tax Exemption
For tax-exempt organizations:
1. Contact support with exemption documentation
2. We'll update your account
3. Future invoices will exclude tax
## Cancellation
To cancel your subscription:
1. Go to **Settings** > **Billing**
2. Click **Cancel Subscription**
3. Select reason (helps us improve)
4. Confirm cancellation
After cancellation:
* Access continues until end of billing period
* Data retained for 30 days
* API keys stop working at period end
* Download any needed data before then
# Batch Processing
Source: https://docs.pictify.io/guides/batch-processing
Generate images at scale with batch operations
# Batch Processing
Batch operations let you generate hundreds or thousands of images efficiently from a single API call. Perfect for bulk social cards, certificates, personalized content, and data-driven graphics.
## When to Use Batch
| Use Case | Single API | Batch API |
| ------------- | ------------- | ---------- |
| 1-10 images | ✅ Simple | ❌ Overkill |
| 10-100 images | ⚠️ Slow | ✅ Better |
| 100+ images | ❌ Rate limits | ✅ Required |
## Basic Batch Workflow
### 1. Prepare Your Data
Structure your data as an array of variable objects:
```typescript theme={null}
const posts = [
{ title: 'Getting Started with APIs', author: 'Alice', date: '2026-01-15' },
{ title: 'Advanced JavaScript', author: 'Bob', date: '2026-01-20' },
{ title: 'Cloud Architecture', author: 'Charlie', date: '2026-01-25' },
// ... hundreds more
];
```
### 2. Start the Batch Job
Batch rendering is **asynchronous** — `renderBatch` returns immediately (HTTP 202) with a `batchId`.
```typescript theme={null}
import { Pictify } from '@pictify/sdk';
const pictify = new Pictify({ apiKey: process.env.PICTIFY_API_KEY! });
const job = await pictify.renderBatch({
templateId: 'tmpl_blog_card',
variableSets: posts.map(post => ({
title: post.title,
author: post.author,
publishDate: post.date
})), // max 100 per batch
format: 'png',
});
console.log(`Batch started: ${job.batchId}`);
console.log(`Processing ${job.totalItems} images`);
```
### 3. Monitor Progress
```typescript theme={null}
// Poll for status
const checkStatus = async (batchId: string) => {
const status = await pictify.getBatchResults(batchId);
console.log(`Progress: ${status.completedItems}/${status.totalItems}`);
console.log(`Failed: ${status.failedItems}`);
return status.status;
};
// Check every 5 seconds
while (true) {
const status = await checkStatus(job.batchId);
if (['completed', 'partial', 'failed', 'cancelled'].includes(status)) break;
await sleep(5000);
}
```
### 4. Collect Results
The poll endpoint does **not** return rendered URLs. `getBatchResults` reports per-item `{ index, success, variables }` (plus `error` on failures). Final image URLs are delivered via the `render.completed` webhook — subscribe to webhooks to collect batch output (see below).
```typescript theme={null}
const status = await pictify.getBatchResults(job.batchId);
for (const item of status.results) {
if (item.success) {
console.log(`Item ${item.index} rendered (vars: ${item.variables.join(', ')})`);
} else {
console.error(`Item ${item.index} failed: ${item.error}`);
}
}
```
## Webhook Integration
Webhooks are how you collect the rendered image URLs from a batch. Configure a webhook subscription in the dashboard (**Settings** > **Webhooks**) or via the [webhook API](/concepts/webhooks), then handle two event types:
* `render.completed` — fired once **per rendered image**, and carries the `url`. This is where you collect output.
* `batch.completed` — fired once when the whole job finishes, with success/failure counts.
### Setup Webhook Handler
```typescript theme={null}
// routes/webhooks.ts — verify the signature first (see /security/webhook-verification)
app.post('/webhooks/pictify', express.raw({ type: 'application/json' }), async (req, res) => {
const event = JSON.parse(req.body.toString());
switch (event.event) {
case 'render.completed':
// Each completed render carries its URL — persist it.
await saveImageUrl(event.data.variables?.title, event.data.url);
break;
case 'batch.completed': {
const { batchId, completedCount, failedCount } = event.data;
console.log(`Batch ${batchId} done — success: ${completedCount}, failed: ${failedCount}`);
break;
}
}
res.sendStatus(200);
});
```
### render.completed Payload
The render events deliver the URLs (the batch poll does not):
```json theme={null}
{
"event": "render.completed",
"timestamp": "2026-01-29T10:30:00Z",
"data": {
"type": "image",
"source": "api",
"imageId": "img_abc123",
"url": "https://cdn.pictify.io/renders/abc123.png",
"templateId": "tmpl_xyz789",
"variables": { "title": "Getting Started with APIs" },
"renderedAt": "2026-01-29T10:30:00Z"
}
}
```
### batch.completed Payload
```json theme={null}
{
"event": "batch.completed",
"timestamp": "2026-01-29T10:30:00Z",
"data": {
"batchId": "batch_abc123",
"templateUid": "tmpl_xyz789",
"status": "completed",
"totalCount": 500,
"completedCount": 498,
"failedCount": 2,
"duration": 45000
}
}
```
## Real-World Examples
### Social Cards for Blog Posts
```typescript theme={null}
// Fetch all posts from your CMS
const posts = await cms.getPosts({ limit: 1000 });
// Prepare variable sets
const variableSets = posts.map(post => ({
title: post.title,
excerpt: post.excerpt.substring(0, 120),
author: post.author.name,
authorAvatar: post.author.avatarUrl,
category: post.category.name,
readTime: `${post.readTime} min read`,
publishDate: formatDate(post.publishedAt)
}));
// Start batch (URLs arrive later via the render.completed webhook)
const job = await pictify.renderBatch({
templateId: 'tmpl_social_card',
variableSets,
format: 'png',
});
// Store batch ID for tracking
await db.batches.create({
batchId: job.batchId,
type: 'social-cards',
totalCount: posts.length,
status: 'processing'
});
```
### Event Certificates
```typescript theme={null}
const attendees = [
{ name: 'Alice Johnson', email: 'alice@example.com', ticketId: 'TK001' },
{ name: 'Bob Smith', email: 'bob@example.com', ticketId: 'TK002' },
// ... more attendees
];
const job = await pictify.renderBatch({
templateId: 'tmpl_certificate',
variableSets: attendees.map(a => ({
recipientName: a.name,
eventName: 'Tech Conference 2026',
eventDate: 'January 29, 2026',
certificateId: `CERT-${a.ticketId}`
})),
format: 'png',
});
// Send each certificate as its render.completed webhook arrives —
// that event carries the rendered URL (the batch poll does not).
async function onRenderCompleted(data: { url: string; variables?: Record }) {
const certificateId = data.variables?.certificateId;
const attendee = attendees.find(a => `CERT-${a.ticketId}` === certificateId);
if (!attendee) return;
await sendEmail({
to: attendee.email,
subject: 'Your Conference Certificate',
body: `Download your certificate: ${data.url}`
});
}
```
### Product Images
```typescript theme={null}
const products = await db.products.findAll();
const job = await pictify.renderBatch({
templateId: 'tmpl_product_card',
variableSets: products.map(p => ({
productName: p.name,
price: formatCurrency(p.price),
originalPrice: p.salePrice ? formatCurrency(p.originalPrice) : null,
discount: p.salePrice ? `${p.discountPercent}% OFF` : null,
imageUrl: p.imageUrl,
rating: p.rating,
reviewCount: p.reviewCount,
badge: p.isNew ? 'NEW' : p.isBestseller ? 'BESTSELLER' : null
})),
format: 'jpeg',
quality: 0.9 // template render quality: 0.1–1.0
});
```
## Handling Failures
### Identify Failed Items
```typescript theme={null}
const status = await pictify.getBatchResults(batchId);
const failed = status.results.filter(item => !item.success);
console.log(`${failed.length} items failed:`);
for (const item of failed) {
console.log(` Index ${item.index}: ${item.error}`);
console.log(` Variables: ${JSON.stringify(item.variables)}`);
}
```
### Retry Failed Items
```typescript theme={null}
async function retryFailedItems(templateId: string, batchId: string) {
const status = await pictify.getBatchResults(batchId);
const failed = status.results.filter(item => !item.success);
if (failed.length === 0) {
console.log('No failed items to retry');
return;
}
// The poll response reports the variable names per item, not the original
// values — look the failed sets back up from your own records.
const failedVariableSets = failed.map(item => originalVariableSets[item.index]);
// Start a new batch with just the failed items
const retryJob = await pictify.renderBatch({
templateId,
variableSets: failedVariableSets,
});
console.log(`Retry batch started: ${retryJob.batchId}`);
}
```
### Common Failure Reasons
| Error | Cause | Solution |
| -------------------- | ------------------------- | ------------------------ |
| `INVALID_VARIABLE` | Missing required variable | Check data completeness |
| `EXPRESSION_ERROR` | Invalid expression syntax | Fix template expressions |
| `IMAGE_FETCH_FAILED` | Can't load external image | Verify image URLs |
| `RENDER_TIMEOUT` | Complex template | Simplify template |
## Performance Tips
### Optimize Template
1. **Simplify CSS** - Avoid complex gradients and shadows
2. **Preload fonts** - Use web-safe fonts or inline font data
3. **Optimize images** - Use appropriately sized source images
4. **Reduce elements** - Fewer DOM elements = faster render
### Batch Size
A single batch accepts up to **100 variable sets**. Split larger datasets across multiple batches.
| Items | Recommendation |
| ----- | ------------------------- |
| ≤ 100 | Single batch |
| > 100 | Split into batches of 100 |
```typescript theme={null}
// Split large datasets (max 100 variable sets per batch)
const BATCH_SIZE = 100;
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const chunk = items.slice(i, i + BATCH_SIZE);
const job = await pictify.renderBatch({
templateId,
variableSets: chunk,
});
await db.batches.create({ batchId: job.batchId, chunkIndex: i / BATCH_SIZE });
}
```
### Parallel Processing
Pictify processes batch items in parallel. Larger batches are more efficient than multiple small batches.
## Monitoring
### Track Batch Progress
```typescript theme={null}
interface BatchMetrics {
batchId: string;
startedAt: Date;
completedAt?: Date;
totalCount: number;
successCount: number;
failedCount: number;
avgRenderTime?: number;
}
async function trackBatch(batchId: string): Promise {
const status = await pictify.getBatchResults(batchId);
return {
batchId,
startedAt: new Date(status.createdAt),
completedAt: status.completedAt ? new Date(status.completedAt) : undefined,
totalCount: status.totalItems,
successCount: status.completedItems,
failedCount: status.failedItems,
avgRenderTime: status.completedAt
? (new Date(status.completedAt).getTime() -
new Date(status.createdAt).getTime()) / status.totalItems
: undefined
};
}
```
### Set Up Alerts
```typescript theme={null}
async function onBatchComplete(event: BatchCompleteEvent) {
const { completedCount, failedCount, totalCount } = event.data;
const failureRate = failedCount / totalCount;
if (failureRate > 0.05) { // > 5% failure rate
await sendAlert({
type: 'batch_high_failure_rate',
message: `Batch ${event.data.batchId} had ${(failureRate * 100).toFixed(1)}% failure rate`,
batchId: event.data.batchId
});
}
}
```
# Webhook Integration
Source: https://docs.pictify.io/guides/webhook-integration
Build real-time integrations with Pictify webhooks
# Webhook Integration
Webhooks let you build real-time integrations that respond to Pictify events. Instead of polling for changes, receive instant notifications when renders complete, fail, or bindings update.
## Use Cases
* **Update database** when images are ready
* **Trigger workflows** after batch completion
* **Send notifications** on render failures
* **Sync with CDN** when content changes
* **Log analytics** for monitoring
## Quick Start
### 1. Create Endpoint
Build a webhook receiver:
Pictify signs every delivery; verify the signature with a small HMAC helper (the SDKs do not ship a webhook helper — roll your own as shown in [Webhook Verification](/security/webhook-verification)).
```typescript theme={null}
// Express.js
import express from 'express';
import crypto from 'crypto';
const app = express();
function verifyWebhookSignature(payload: string, signatureHeader: string, secret: string): boolean {
const parts: Record = {};
for (const pair of signatureHeader.split(',')) {
const [key, value] = pair.split('=');
parts[key] = value;
}
const timestamp = parseInt(parts.t, 10);
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false; // replay protection
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
app.post(
'/webhooks/pictify',
express.raw({ type: 'application/json' }),
async (req, res) => {
// Verify signature
const signature = req.headers['x-pictify-signature'] as string;
const isValid = verifyWebhookSignature(
req.body.toString(),
signature,
process.env.PICTIFY_WEBHOOK_SECRET!
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Parse and handle event
const event = JSON.parse(req.body.toString());
await handleEvent(event);
res.status(200).send('OK');
}
);
async function handleEvent(event: WebhookEvent) {
switch (event.event) {
case 'render.completed':
await onRenderCompleted(event.data);
break;
case 'render.failed':
await onRenderFailed(event.data);
break;
case 'batch.completed':
await onBatchCompleted(event.data);
break;
case 'binding.updated':
await onBindingUpdated(event.data);
break;
}
}
```
### 2. Subscribe to Events
Webhook subscriptions are managed in the dashboard (**Settings** > **Webhooks**) or via the REST API. The SDKs do not expose webhook methods. Create a subscription with cURL:
```bash theme={null}
curl -X POST https://api.pictify.io/webhook-subscriptions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event": "render.completed",
"targetUrl": "https://yoursite.com/webhooks/pictify",
"platform": "custom"
}'
```
The response includes the signing `secret` (shown only once) — store it for signature verification:
```json theme={null}
{
"subscription": {
"uid": "wh_abc123",
"event": "render.completed",
"targetUrl": "https://yoursite.com/webhooks/pictify",
"status": "active",
"secret": "whsec_xyz789..."
}
}
```
### 3. Test Your Endpoint
Use the dashboard to send a test webhook:
1. Go to **Settings** > **Webhooks**
2. Find your subscription
3. Click **Send Test**
4. Verify your endpoint received it
## Event Types
### render.completed
Fired when an image, GIF, or PDF finishes rendering.
```json theme={null}
{
"event": "render.completed",
"timestamp": "2026-01-29T10:30:00Z",
"data": {
"type": "image",
"source": "api",
"imageId": "img_abc123",
"url": "https://cdn.pictify.io/renders/abc123.png",
"userStorageUrl": "https://your-bucket.s3.amazonaws.com/abc123.png",
"width": 1200,
"height": 630,
"format": "png",
"templateId": "tmpl_xyz789",
"variables": {
"title": "Hello World"
},
"renderedAt": "2026-01-29T10:30:00Z"
}
}
```
**Use cases:**
* Update CMS with image URL
* Invalidate CDN cache
* Notify users their image is ready
### render.failed
Fired when a render fails.
```json theme={null}
{
"event": "render.failed",
"timestamp": "2026-01-29T10:30:00Z",
"data": {
"type": "image",
"source": "api",
"templateId": "tmpl_xyz789",
"error": "Template not found",
"errorCode": "TEMPLATE_NOT_FOUND",
"variables": {
"title": "Hello World"
}
}
}
```
**Use cases:**
* Alert on failures
* Retry with different parameters
* Log for debugging
### batch.completed
Fired when a batch job finishes.
```json theme={null}
{
"event": "batch.completed",
"timestamp": "2026-01-29T10:30:00Z",
"data": {
"batchId": "batch_abc123",
"templateUid": "tmpl_xyz789",
"status": "completed",
"totalCount": 500,
"completedCount": 498,
"failedCount": 2,
"duration": 45000
}
}
```
**Use cases:**
* Process batch results
* Send completion notification
* Trigger next workflow step
### binding.updated
Fired when a binding successfully refreshes.
```json theme={null}
{
"event": "binding.updated",
"timestamp": "2026-01-29T10:30:00Z",
"data": {
"bindingId": "bind_abc123",
"templateUid": "tmpl_xyz789",
"imageUrl": "https://cdn.pictify.io/bindings/abc123.png",
"previousData": { "stars": 100 },
"newData": { "stars": 105 }
}
}
```
**Use cases:**
* Invalidate cached pages
* Log data changes
* Trigger dependent updates
### binding.failed
Fired when a binding fails to refresh.
```json theme={null}
{
"event": "binding.failed",
"timestamp": "2026-01-29T10:30:00Z",
"data": {
"bindingId": "bind_abc123",
"error": "Data source returned 500",
"retryCount": 3,
"nextRetry": "2026-01-29T11:00:00Z"
}
}
```
## Filtering Events
Subscribe only to events you care about:
### Filter by Template
```bash theme={null}
curl -X POST https://api.pictify.io/webhook-subscriptions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event": "render.completed",
"targetUrl": "https://yoursite.com/webhooks/blog-cards",
"filters": { "templateId": "tmpl_blog_card" }
}'
```
### Filter by Type
```bash theme={null}
curl -X POST https://api.pictify.io/webhook-subscriptions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event": "render.completed",
"targetUrl": "https://yoursite.com/webhooks/images-only",
"filters": { "type": "image" }
}'
```
## Handler Patterns
### Database Updates
```typescript theme={null}
async function onRenderCompleted(data: RenderCompletedData) {
// Update your content with the image URL
await db.content.update({
where: { id: data.variables.contentId },
data: { ogImageUrl: data.url }
});
}
```
### Cache Invalidation
```typescript theme={null}
async function onBindingUpdated(data: BindingUpdatedData) {
// Invalidate CDN cache for pages using this image
await cdn.invalidate([
`/images/${data.bindingId}`,
`/pages/*`
]);
}
```
### Slack Notifications
```typescript theme={null}
async function onRenderFailed(data: RenderFailedData) {
await slack.postMessage({
channel: '#alerts',
text: `🚨 Render failed: ${data.error}`,
attachments: [{
color: 'danger',
fields: [
{ title: 'Template', value: data.templateId },
{ title: 'Error Code', value: data.errorCode }
]
}]
});
}
```
### Workflow Orchestration
```typescript theme={null}
async function onBatchCompleted(data: BatchCompletedData) {
if (data.failedCount > 0) {
// Retry failed items
await retryFailedItems(data.batchId);
}
if (data.status === 'completed') {
// Trigger next step
await startEmailCampaign(data.batchId);
}
}
```
## Error Handling
### Idempotent Handlers
Webhooks may be delivered multiple times. Make handlers idempotent:
```typescript theme={null}
async function onRenderCompleted(data: RenderCompletedData) {
const deliveryId = data.deliveryId;
// Check if already processed
const existing = await db.processedWebhooks.findUnique({
where: { deliveryId }
});
if (existing) {
console.log(`Already processed: ${deliveryId}`);
return;
}
// Process the webhook
await db.content.update({
where: { id: data.variables.contentId },
data: { ogImageUrl: data.url }
});
// Mark as processed
await db.processedWebhooks.create({
data: { deliveryId, processedAt: new Date() }
});
}
```
### Graceful Degradation
Handle errors without crashing:
```typescript theme={null}
app.post('/webhooks/pictify', async (req, res) => {
try {
const event = JSON.parse(req.body.toString());
// Process with timeout
await Promise.race([
handleEvent(event),
timeout(25000) // 25 second timeout
]);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook error:', error);
// Return 200 to prevent retries for unrecoverable errors
if (error.isRetryable) {
res.status(500).send('Retry later');
} else {
res.status(200).send('Acknowledged with error');
}
}
});
```
### Queue Processing
For heavy workloads, queue webhooks:
```typescript theme={null}
app.post('/webhooks/pictify', async (req, res) => {
const event = JSON.parse(req.body.toString());
// Add to queue immediately
await queue.add('pictify-webhook', event);
// Return quickly
res.status(200).send('Queued');
});
// Process asynchronously
queue.process('pictify-webhook', async (job) => {
await handleEvent(job.data);
});
```
## Security
### Always Verify Signatures
```typescript theme={null}
const isValid = verifyWebhookSignature(
payload,
signatureHeader,
secret
);
if (!isValid) {
throw new Error('Invalid webhook signature');
}
```
### Use HTTPS
Always use HTTPS endpoints in production:
```
✅ https://api.yoursite.com/webhooks/pictify
❌ http://api.yoursite.com/webhooks/pictify
```
### Validate Event Data
```typescript theme={null}
function validateRenderCompleted(data: unknown): data is RenderCompletedData {
return (
typeof data === 'object' &&
data !== null &&
'imageId' in data &&
'url' in data
);
}
async function handleEvent(event: WebhookEvent) {
if (event.event === 'render.completed') {
if (!validateRenderCompleted(event.data)) {
throw new Error('Invalid render.completed payload');
}
await onRenderCompleted(event.data);
}
}
```
## Debugging
### Webhook Logs
View webhook delivery logs in the dashboard:
1. Go to **Settings** > **Webhooks**
2. Click on a subscription
3. View **Delivery Logs**
Each log shows:
* Timestamp
* Response status
* Response body
* Response time
### Test Mode
Send a test webhook without triggering a real event from the dashboard:
1. Go to **Settings** > **Webhooks**
2. Select your subscription
3. Click **Send Test**
4. Confirm your endpoint received and verified the delivery
### Local Development
Use ngrok or similar for local testing:
```bash theme={null}
ngrok http 3000
# Use the ngrok URL for webhook subscriptions
```
# n8n
Source: https://docs.pictify.io/integrations/n8n
Generate images, GIFs, and PDFs inside n8n workflows using the official Pictify community node.
The **`n8n-nodes-pictify`** community node lets you call Pictify from any n8n workflow — no code required. Render Open Graph cards, social media images, animated GIFs, certificates, invoices, and more, then chain into email, Slack, S3, Notion, Airtable, or anything else n8n supports.
Community nodes work on self-hosted n8n and on n8n Cloud (Pro plan and above). See [n8n's community-node docs](https://docs.n8n.io/integrations/community-nodes/) for details.
## Install
In your n8n instance:
1. Open **Settings → Community Nodes**.
2. Click **Install**.
3. Enter the package name: `n8n-nodes-pictify`
4. Accept the prompt and wait for install to complete.
The **Pictify** node now appears in the node picker.
## Connect credentials
1. Get an API key at [pictify.io](https://pictify.io) → Settings → API Keys.
2. In n8n: **Credentials → New → Pictify API**.
3. Paste your API key. Leave **Base URL** at the default unless you self-host.
4. Click **Test** — n8n calls `GET /templates` to verify the key.
## Available operations
| Resource | Operation | What it does |
| ------------ | -------------------- | ---------------------------------------------------------- |
| **Image** | Render From Template | Render a saved template with variables |
| **Image** | Render From HTML | Render raw HTML + CSS into an image |
| **Image** | Render Batch | Render up to 500 images from one template in a single call |
| **GIF** | Render GIF | Render an animated GIF from a template or HTML with frames |
| **PDF** | Render PDF | Render a PDF (page format, margins, landscape orientation) |
| **Template** | Get | Fetch a single template and its variables |
| **Template** | List | List all templates on your account |
Every render operation supports:
* **Return Binary** — download the rendered file as a binary item, ready to email or upload.
* **Continue On Fail** — let the workflow keep running even if a single item fails.
* **Layout / Layouts** — render one or many layout variants from a multi-layout template.
## Example workflows
**Open Graph card per blog post**
```
Postgres (rows) → Pictify (Render From Template) → Set (image_url) → Postgres (update)
```
**Personalised certificate via email**
```
Webhook → Pictify (Render PDF, Return Binary = true) → Gmail (attachment)
```
**Slack ship-it GIF on every release**
```
GitHub Trigger (release) → Pictify (Render GIF) → Slack (post message)
```
**Bulk social card generation**
```
Google Sheets (rows) → Pictify (Render Batch) → Google Drive (upload URLs)
```
## Source & support
* **npm**: [`n8n-nodes-pictify`](https://www.npmjs.com/package/n8n-nodes-pictify)
* **GitHub**: [`pictify-io/n8n-nodes-pictify`](https://github.com/pictify-io/n8n-nodes-pictify)
* **Issues**: open one on GitHub or email [support@pictify.io](mailto:support@pictify.io)
# Introduction
Source: https://docs.pictify.io/introduction
Pictify API: generate images (PNG, JPG, WebP), multi-page PDFs, GIFs, and MP4 video from HTML templates — emailed to each recipient with delivery status
# Welcome to Pictify
Pictify is an API for generating images (PNG, JPG, WebP), multi-page PDFs, animated GIFs, and MP4 video from HTML templates. Workflow runs connect a CSV upload or webhook to a template and email each rendered document to its recipient, with delivered/bounced status tracked per row — we call this pattern **Render-to-Recipient**. Use it for certificates, invoices, reports, social media cards, Open Graph images, and personalized video at scale.
## What You Can Build
Generate dynamic Open Graph images for social media sharing
Create personalized marketing materials at scale
Render animated content from HTML/CSS animations
Generate invoices, reports, and certificates
Render MP4 video from templates — per-recipient variants with one API call
Workflow runs email each render to its recipient with per-row delivery status
## How It Works
1. **Create a Template** - Design your template in the Pictify dashboard using the visual editor or import HTML/CSS
2. **Define Variables** - Mark dynamic content like text, images, and colors as variables
3. **Render via API** - Call the API with your template ID and variable values
4. **Get Your Asset** - Receive a URL to your generated image, GIF, or PDF
```typescript theme={null}
import { Pictify } from '@pictify/sdk';
const pictify = new Pictify({
apiKey: process.env.PICTIFY_API_KEY
});
const result = await pictify.render({
templateId: 'og-image-template',
variables: {
title: 'My Blog Post',
author: 'Jane Doe',
date: '2026-01-29'
}
});
console.log(result.url); // results[0].url
```
## Key Features
### Template System
Create reusable templates with the visual editor or HTML/CSS. Define variables for dynamic content and use expressions for conditional logic.
### Multiple Output Formats
Generate PNG, JPEG, WebP images, animated GIFs, and multi-page PDFs from the same templates.
### Batch Processing
Render up to 500 images in a single API request. Process thousands of assets efficiently for data-driven campaigns.
### Official SDKs
Native SDKs for Node.js, Python, Go, and Ruby with full TypeScript/type hints support.
### Webhooks
Receive notifications when renders complete. Integrate with Zapier, Make, n8n, or your own systems.
### AI Integration
MCP server for Claude Code, Cursor, and other AI tools. Generate images directly from AI conversations.
## Getting Started
Get your first image in 5 minutes
Explore the full API documentation
Install an official SDK
Learn about the template system
## Need Help?
* **Email**: [support@pictify.io](mailto:support@pictify.io)
* **Discord**: [Join our community](https://discord.gg/pictify)
* **GitHub**: [Report issues](https://github.com/pictify-io/pictify)
# Quickstart
Source: https://docs.pictify.io/quickstart
Generate your first image with the Pictify API in 5 minutes — from API key to CDN-hosted PNG
# Quickstart
This guide walks you through generating your first image with Pictify in under 5 minutes.
## Prerequisites
* A Pictify account ([sign up free](https://pictify.io/signup))
* An API key (available in your [dashboard](https://pictify.io/dashboard/settings))
## Step 1: Get Your API Key
1. Log in to your [Pictify dashboard](https://pictify.io/dashboard)
2. Navigate to **Settings** > **API Keys**
3. Click **Create API Key**
4. Copy and store your key securely
Keep your API key secret. Never expose it in client-side code or public repositories.
## Step 2: Install an SDK (Optional)
Choose your preferred language:
```bash npm theme={null}
npm install @pictify/sdk
```
```bash pip theme={null}
pip install pictify
```
```bash go theme={null}
go get github.com/pictify-io/pictify-go
```
```bash gem theme={null}
gem install pictify
```
## Step 3: Generate an Image
### Using the SDK
```typescript Node.js theme={null}
import { Pictify } from '@pictify/sdk';
const pictify = new Pictify({
apiKey: 'your-api-key'
});
const image = await pictify.renderHtml({
html: `
",
"width": 1200,
"height": 630
}'
```
## Step 4: Use a Template
Templates let you create reusable designs with variables. Create a template in the dashboard, then render it:
```typescript Node.js theme={null}
const result = await pictify.render({
templateId: 'your-template-uid',
variables: {
title: 'My Blog Post',
author: 'Jane Doe',
publishedAt: '2026-01-29'
}
});
console.log(result.url); // results[0].url
```
```python Python theme={null}
result = client.render(
"your-template-uid",
variables={
"title": "My Blog Post",
"author": "Jane Doe",
"publishedAt": "2026-01-29"
}
)
print(result.url) # results[0].url
```
## Next Steps
Learn to create reusable templates with variables
Explore all API endpoints
Full SDK documentation
Generate images at scale
# Changelog
Source: https://docs.pictify.io/resources/changelog
Recent updates and improvements to Pictify
# Changelog
Stay up to date with the latest features, improvements, and fixes.
## August 2026
**Video Templates**
* Video template API: build a template once (visual timeline studio or a single-file Remotion scene), render it with variables per request — `POST /video/templates/:uid/render`
* GIF output from the video engine: the same render call takes `format: "gif"` — palette-optimised (timeline templates: 15fps / 720px caps; code templates encode natively at half the composition frame rate)
* AI template generation: `POST /video/templates/generate` designs a motion brief, writes the scene code, compiles it, renders frames and reviews them visually before returning a draft template
* AI copilot in the studio: describe a change — or a whole video — and it is designed, built with validated edits, and reviewed, with one-click undo per run
* Captions from speech, stock media search, effects, keyframes, transitions and a font library in the timeline studio
* Server-rendered posters: every code template gets a thumbnail automatically
**Workflows**
* Inbound hooks: a public URL per workflow that accepts rows from any system, with optional HMAC signature verification (`X-Pictify-Signature`)
* Run stats and per-row progress (`total / rendered / delivered / failed`)
* Saved-template plan limits enforced server-side, shared across image and video templates
**Improvements**
* API Playground reorganised by product — HTML Rendering, Image Templates, Batch, PDF, Video — with PDF and video endpoints included for the first time
* Documentation corrected against the live API: real key format, real error shapes, real rate-limit behaviour
## January 2026
### January 29, 2026
**New Features**
* Agent Screenshot API - AI-powered screenshots using natural language prompts
* MCP Server - Model Context Protocol support for AI agent integration
* Multi-page PDF generation from templates
**Improvements**
* 40% faster GIF generation
* Improved error messages across generation endpoints
* Better WebP compression quality
### January 15, 2026
**New Features**
* Bindings API for automatic image updates
* Webhook filters for targeted event subscriptions
* Ruby SDK v1.0.0 released
**Improvements**
* Template editor performance improvements
* Dashboard usage analytics enhancements
***
## December 2025
### December 20, 2025
**New Features**
* FabricJS canvas rendering endpoint
* PDF presets for standard paper sizes
* Go SDK v1.0.0 released
**Improvements**
* Rate limit headers on all responses
* Improved template variable extraction
### December 5, 2025
**New Features**
* Batch rendering with webhook notifications
* Template version history
* Python SDK async support
**Fixes**
* Fixed timeout issues with complex templates
* Resolved font loading for certain Google Fonts
***
## November 2025
### November 22, 2025
**New Features**
* Custom fonts in templates
* Expression engine with 40+ built-in functions
* Idempotency support for POST requests
**Improvements**
* Reduced image generation latency by 25%
* Better handling of large HTML content
### November 8, 2025
**New Features**
* GIF capture from URLs
* Webhook signature verification
* Node.js SDK v2.0.0 with TypeScript support
**Fixes**
* Fixed transparency in WebP images
* Resolved CSS Grid rendering issues
***
## October 2025
### October 25, 2025
**New Features**
* Templates API with variable interpolation
* Retina/high-DPI image support
* Dashboard template editor
### October 10, 2025
**Initial Release**
* Image generation from HTML/CSS
* Screenshot from URL
* PNG, JPEG, WebP format support
* Node.js and Python SDKs
***
## Upgrade Notices
### API v1 Stability
API v1 is now stable. We commit to backward compatibility for all documented endpoints. Breaking changes will only be introduced in new API versions.
### SDK Updates
We recommend keeping SDKs updated for the latest features and security patches:
```bash theme={null}
# Node.js
npm update @pictify/sdk
# Python
pip install --upgrade pictify
# Go
go get -u github.com/pictify/pictify-go
# Ruby
bundle update pictify
```
## Deprecations
### Planned Deprecations
| Feature | Deprecated | Removal Date | Alternative |
| -------------- | ---------- | ------------ | ----------- |
| None currently | - | - | - |
### Recently Removed
| Feature | Removed | Alternative |
| ------- | ------- | ----------- |
| None | - | - |
## Feature Requests
Have a feature request? We'd love to hear from you:
* [GitHub Discussions](https://github.com/pictify/pictify/discussions)
* [Email Support](mailto:support@pictify.io)
## Status Page
Check service status and subscribe to updates:
[status.pictify.io](https://status.pictify.io)
# Glossary
Source: https://docs.pictify.io/resources/glossary
Key terms and definitions
# Glossary
A reference guide to terms used throughout the Pictify documentation.
## A
### API Key
A secret token used to authenticate requests to the Pictify API — a 64-character hex string passed as a Bearer token. Every key is live; there is no separate test-key type.
### Agent Screenshot
An AI-powered feature that uses natural language prompts to navigate to a webpage and capture specific content automatically.
## B
### Batch Operation
Processing multiple renders in a single API request. More efficient than individual requests for generating many images.
### Binding
A connection between a template and an external data source that automatically refreshes the rendered image when data changes.
## C
### Canvas Rendering
Generating images from FabricJS canvas JSON data, enabling programmatic image creation with objects, text, and shapes.
### CDN (Content Delivery Network)
A globally distributed network that serves rendered images from locations close to users for faster loading.
## D
### Device Scale Factor
A multiplier for output resolution. A factor of 2 produces "retina" images at 2x the specified dimensions.
## E
### Expression
Dynamic code within templates using `{{...}}` syntax. Supports variables, operators, and built-in functions.
### Expression Engine
The system that evaluates expressions in templates, providing features like string formatting, math operations, and conditionals.
## F
### FabricJS
An open-source JavaScript canvas library. Pictify can render FabricJS canvas JSON directly to images.
## G
### GIF Capture
Recording an animated GIF from a live webpage, capturing CSS animations or JavaScript-driven motion.
## H
### HMAC-SHA256
The cryptographic algorithm used to sign webhooks, ensuring they originate from Pictify and haven't been tampered with.
### HTML Rendering
Converting HTML and CSS code into image formats like PNG, JPEG, or WebP.
## I
### Idempotency
The ability to safely retry requests without duplicate effects. Enabled by the `Idempotency-Key` header.
### Idempotency Key
A unique identifier included in requests to ensure they're only processed once, even if sent multiple times.
## M
### MCP (Model Context Protocol)
A standard protocol for connecting AI models to external tools. Pictify's MCP server enables AI agents to generate images.
## O
### OG Image (Open Graph Image)
The preview image shown when content is shared on social media platforms like Facebook, Twitter, and LinkedIn.
### OpenAPI
A specification for documenting REST APIs. Pictify provides an OpenAPI 3.1 spec for API reference.
## P
### PDF Preset
Standard paper sizes for PDF generation (A4, Letter, Legal, etc.) with predefined dimensions.
### Problem Details (RFC 9457)
A standard format for API error responses that includes type, title, status, and detail fields.
## R
### Rate Limit
Restrictions on how many API requests can be made in a time period. Varies by plan (Free: 60/min, Pro: 300/min, etc.).
### Render
The process of converting templates, HTML, or URLs into images, GIFs, or PDFs.
### Retina Image
A high-resolution image (typically 2x or 3x) optimized for high-DPI displays like Apple Retina screens.
### RFC 9457
The IETF standard for "Problem Details for HTTP APIs" used by Pictify's error responses.
## S
### Selector
A CSS selector used to capture a specific element on a page rather than the full viewport.
### Signing Secret
A cryptographic secret used to verify webhook signatures. Unique to each webhook subscription.
### SSRF (Server-Side Request Forgery)
A security vulnerability where attackers trick servers into making unintended requests. Pictify blocks private network URLs to prevent SSRF.
## T
### Template
A reusable design with variable placeholders that can be rendered with different data.
### Template Variable
A placeholder in a template (like `{{title}}`) that gets replaced with actual data during rendering.
### Tool Schema
JSON schema definitions for AI tool integration, describing available functions and their parameters.
### Transparent Background
PNG or WebP images with alpha channel transparency instead of a solid background.
## U
### User Storage
Optional cloud storage (S3, R2, etc.) where Pictify can upload rendered images in addition to the CDN.
## V
### Variable Interpolation
The process of replacing template placeholders (`{{variable}}`) with actual values during rendering.
### Variable Set
A collection of variable values used to render a template. Batch operations accept arrays of variable sets.
## W
### Webhook
An HTTP callback that notifies your server when events occur (render completed, batch finished, etc.).
### Webhook Signature
A cryptographic signature in the `X-Pictify-Signature` header used to verify webhook authenticity.
### WebP
A modern image format developed by Google offering better compression than PNG or JPEG while supporting transparency.
## Common Abbreviations
| Abbreviation | Full Term |
| ------------ | -------------------------------------- |
| API | Application Programming Interface |
| CDN | Content Delivery Network |
| CSS | Cascading Style Sheets |
| DPI | Dots Per Inch |
| GIF | Graphics Interchange Format |
| HMAC | Hash-based Message Authentication Code |
| HTML | HyperText Markup Language |
| HTTP | HyperText Transfer Protocol |
| JSON | JavaScript Object Notation |
| JWT | JSON Web Token |
| MCP | Model Context Protocol |
| OG | Open Graph |
| PDF | Portable Document Format |
| PNG | Portable Network Graphics |
| REST | Representational State Transfer |
| SDK | Software Development Kit |
| SSRF | Server-Side Request Forgery |
| URL | Uniform Resource Locator |
| UTC | Coordinated Universal Time |
# Troubleshooting
Source: https://docs.pictify.io/resources/troubleshooting
Common issues and solutions
# Troubleshooting
This guide covers common issues and their solutions when using the Pictify API.
## Authentication Issues
### "Invalid API Key" (401)
**Symptoms:** API returns 401 Unauthorized with "Invalid API Key" message.
**Solutions:**
1. **Verify the key format**
* A valid key is a 64-character hex string with no prefix
2. **Check for extra characters**
```bash theme={null}
# Wrong - has quotes
PICTIFY_API_KEY="your-api-key"
# Correct - no quotes in shell
PICTIFY_API_KEY=your-api-key
```
3. **Verify the key isn't revoked**
* Check in Dashboard > Settings > API Keys
4. **Ensure correct environment**
* Test keys don't work in production mode
### "Missing Authorization" (401)
**Symptoms:** API returns 401 with "Missing Authorization" message.
**Solutions:**
```bash theme={null}
# Make sure header is correct
curl https://api.pictify.io/image \
-H "Authorization: Bearer YOUR_API_KEY" # ✅ Correct
# Common mistakes:
-H "Authorization: YOUR_API_KEY" # ❌ Missing "Bearer"
-H "Api-Key: YOUR_API_KEY" # ❌ Wrong header name
```
## Rendering Issues
### Blank or White Images
**Symptoms:** Generated image is blank or all white.
**Solutions:**
1. **Check HTML has visible content**
```html theme={null}
Hello
Hello
```
2. **Ensure content is within viewport**
* Content outside the width/height won't be captured
3. **Set an explicit background color**
```html theme={null}
Hello
```
### Missing Fonts
**Symptoms:** Text appears in default/fallback font.
**Solutions:**
1. **Use web fonts with @import**
```html theme={null}
```
2. **Use web-safe fonts as fallback**
```css theme={null}
font-family: 'Inter', Arial, sans-serif;
```
3. **Load fonts in the document head**
```html theme={null}
```
### CSS Not Applied
**Symptoms:** Styles are missing or incorrect.
**Solutions:**
1. **Use inline styles for reliability**
```html theme={null}
Hello
```
2. **Check for unsupported CSS**
* Some advanced CSS features may not render correctly
* Test complex layouts in browser first
3. **Avoid external stylesheets on slow domains**
* External CSS must load before rendering
### Images Not Loading
**Symptoms:** Images show as broken or missing.
**Solutions:**
1. **Use absolute URLs**
```html theme={null}
```
2. **Verify URL is accessible**
* Images must be publicly accessible
* Check for CORS issues
3. **Inline critical images as data URIs**
```html theme={null}
```
### Timeout Errors
**Symptoms:** Render fails with timeout error.
**Solutions:**
1. **Simplify the template**
* Remove complex gradients, shadows
* Reduce number of elements
2. **Optimize images**
* Use smaller image files
* Compress before including
3. **Reduce external resources**
* Minimize external CSS/JS
* Self-host critical resources
## Template Issues
### Variables Not Interpolating
**Symptoms:** `{{variable}}` appears literally in output.
**Solutions:**
1. **Check variable syntax**
```
✅ {{title}}
❌ {{ title }} // Spaces may cause issues
❌ {title} // Single braces don't work
```
2. **Verify variable is provided**
```typescript theme={null}
// Make sure all variables are passed
await pictify.render({
templateId: 'tmpl_id',
variables: {
title: 'Hello' // Must match {{title}}
}
});
```
3. **Check for typos**
* Variable names are case-sensitive
* `{{Title}}` !== `{{title}}`
### Expression Errors
**Symptoms:** Expression fails to evaluate.
**Solutions:**
1. **Test expressions separately**
```bash theme={null}
curl -X POST https://api.pictify.io/templates/expression/test \
-H "Authorization: Bearer $API_KEY" \
-d '{
"expression": "currency(price, 'USD')",
"variables": {"price": 99.99}
}'
```
2. **Check function syntax**
```
✅ {{currency(price, 'USD')}}
❌ {{currency(price, USD)}} // String needs quotes
```
3. **Ensure variables exist**
* Undefined variables cause expression errors
## Rate Limiting
### "Rate Limit Exceeded" (429)
**Symptoms:** API returns 429 Too Many Requests.
**Solutions:**
1. **Check headers for limit info**
```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706515320
Retry-After: 45
```
2. **Implement exponential backoff**
```typescript theme={null}
async function withRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.status === 429) {
const delay = error.retryAfter || Math.pow(2, i) * 1000;
await sleep(delay);
continue;
}
throw error;
}
}
}
```
3. **Use batch API for bulk operations**
* Single batch request vs. many individual requests
4. **Consider upgrading plan**
* Higher tiers have higher limits
## Webhook Issues
### Webhooks Not Received
**Symptoms:** Webhook endpoint never receives requests.
**Solutions:**
1. **Verify endpoint is accessible**
```bash theme={null}
curl -X POST https://yoursite.com/webhooks/pictify \
-H "Content-Type: application/json" \
-d '{"test": true}'
```
2. **Check firewall/security rules**
* Allow incoming requests from Pictify IPs
* Ensure HTTPS certificate is valid
3. **Verify subscription is active**
* Check in Dashboard > Settings > Webhooks
* Status should be "active"
### Signature Verification Failing
**Symptoms:** Webhook signature doesn't match.
**Solutions:**
1. **Use raw body for verification**
```typescript theme={null}
// ❌ Wrong
const payload = JSON.stringify(req.body);
// ✅ Correct
const payload = req.body.toString(); // Raw body
```
2. **Check timestamp tolerance**
* Default is 5 minutes
* Ensure server clock is synced
3. **Verify using correct secret**
* Each subscription has a unique secret
* Secret is only shown at creation
## SDK Issues
### Module Not Found
**Symptoms:** `Cannot find module '@pictify/sdk'`
**Solutions:**
```bash theme={null}
# Ensure package is installed
npm install @pictify/sdk
# Clear node_modules and reinstall
rm -rf node_modules package-lock.json
npm install
```
### TypeScript Errors
**Symptoms:** TypeScript compilation errors with SDK.
**Solutions:**
1. **Update TypeScript**
```bash theme={null}
npm install typescript@latest
```
2. **Check tsconfig.json**
```json theme={null}
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true
}
}
```
## Still Need Help?
If you're still experiencing issues:
1. **Check the status page:** [status.pictify.io](https://status.pictify.io)
2. **Search existing issues:** [GitHub Issues](https://github.com/pictify/pictify/issues)
3. **Contact support:** [support@pictify.io](mailto:support@pictify.io)
When reporting issues, include:
* API endpoint and method
* Request payload (without API key)
* Error response
* SDK version (if applicable)
# Go SDK
Source: https://docs.pictify.io/sdks/go
Official Pictify SDK for Go
# Go SDK
The official Pictify SDK for Go provides an idiomatic Go interface for the Pictify API — generate images, PDFs, and GIFs from raw HTML, live URLs, and reusable templates.
## Installation
```bash theme={null}
go get github.com/pictify-io/pictify-go
```
## Quick Start
```go theme={null}
package main
import (
"context"
"fmt"
"log"
"github.com/pictify-io/pictify-go"
)
func main() {
client := pictify.NewClient("your-api-key")
// Render raw HTML to a PNG.
img, err := client.RenderHTML(context.Background(), &pictify.RenderHTMLOptions{
HTML: `
Hello World
`,
Width: 1200,
Height: 630,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(img.URL)
}
```
## API
The SDK talks to the Pictify API at `https://api.pictify.io`. Every call sends `Authorization: Bearer `.
| Method | Endpoint | Returns |
| ----------------- | ------------------------------------ | ----------------------------------------- |
| `RenderHTML` | `POST /image` | `*ImageResult` `{URL, ID, CreatedAt}` |
| `RenderURL` | `POST /image` | `*ImageResult` |
| `Render` | `POST /templates/{uid}/render` | `*RenderResult` (results\[] envelope) |
| `RenderLayouts` | `POST /templates/{uid}/render` | `*RenderResult` |
| `RenderGIF` | `POST /gif` | `*GIFResult` (flattened) |
| `RenderBatch` | `POST /templates/{uid}/batch-render` | `*BatchJob` (async, HTTP 202) |
| `GetBatchResults` | `GET /templates/batch/{id}/results` | `*BatchResults` |
| `GetTemplate` | `GET /templates/{uid}` | `*Template` (unwrapped) |
| `ListTemplates` | `GET /templates` | `*TemplateList` `{Templates, Pagination}` |
| `CreateTemplate` | `POST /templates` | `*Template` (unwrapped) |
## Configuration
`NewClient` takes the API key plus functional options.
```go theme={null}
client := pictify.NewClient(
"your-api-key",
pictify.WithBaseURL("https://api.pictify.io"), // default
pictify.WithTimeout(30*time.Second),
pictify.WithMaxRetries(3),
pictify.WithHTTPClient(&http.Client{
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
},
}),
)
```
Keep your API key secret. Read it from an environment variable (e.g. `os.Getenv("PICTIFY_API_KEY")`) and never expose it in client-side code or public repositories.
## Render an Image from HTML
`POST /image` — returns `{URL, ID, CreatedAt}`.
```go theme={null}
img, err := client.RenderHTML(ctx, &pictify.RenderHTMLOptions{
HTML: `
`,
Width: 400, // default 800
Height: 200, // default 600
Quality: pictify.GIFQualityLow, // low | medium (default) | high
})
if err != nil {
log.Fatal(err)
}
fmt.Println(gif.URL, gif.UID, gif.AnimationLength)
```
To render a GIF from a template, set `TemplateID` (and optionally `Variables`). The source must be animated; a static source returns a render error (HTTP 422).
## Batch Rendering (async)
`POST /templates/{uid}/batch-render` returns immediately (HTTP 202) with a `BatchID`. The job runs asynchronously — poll `GetBatchResults` to track progress.
```go theme={null}
job, err := client.RenderBatch(ctx, &pictify.BatchOptions{
TemplateID: "XL13XACH2V",
VariableSets: []map[string]interface{}{
{"name": "Ada", "company": "Pictify"},
{"name": "Grace", "company": "Pictify"},
},
Format: pictify.FormatPNG,
Concurrency: 5, // 1–10, default 5
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("batch %s: %s (%d items)\n", job.BatchID, job.Status, job.TotalItems)
results, err := client.GetBatchResults(ctx, job.BatchID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("status=%s completed=%d/%d failed=%d\n",
results.Status, results.CompletedItems, results.TotalItems, results.FailedItems)
```
Rendered URLs are **not** returned by the poll endpoint; they are delivered via the `render.completed` webhook. The poll response reports each item's `Index`, `Success`, and variable names (plus an `Error` on failures).
## Template Management
```go theme={null}
// Create a template from HTML. Variables are auto-discovered from {{name}} tokens.
tmpl, err := client.CreateTemplate(ctx, &pictify.CreateTemplateOptions{
HTML: `
Hi {{firstName}}
`,
Name: "Welcome Card",
Width: 600,
Height: 200,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(tmpl.UID)
for _, v := range tmpl.VariableDefinitions {
fmt.Printf(" - %s (%s)\n", v.Name, v.Type)
}
// Fetch a single template by UID.
tmpl, err = client.GetTemplate(ctx, "XL13XACH2V")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %s (%dx%d)\n", tmpl.UID, tmpl.Name, tmpl.Width, tmpl.Height)
// List templates with pagination.
list, err := client.ListTemplates(ctx, &pictify.ListTemplatesOptions{
Page: 1,
Limit: 20, // max 100, default 12
Sort: "newest", // newest | oldest | name
})
if err != nil {
log.Fatal(err)
}
for _, t := range list.Templates {
fmt.Printf("%s: %s\n", t.UID, t.Name)
}
fmt.Printf("page %d of %d (%d total)\n",
list.Pagination.Page, list.Pagination.TotalPages, list.Pagination.Total)
```
`Template` keys by `UID` (not `id`) and declares variables in `VariableDefinitions`. Any unknown engine-specific fields are preserved in `Template.Extra` (a `map[string]json.RawMessage`).
## Error Handling
HTTP errors are mapped to typed errors. Each one embeds `*PictifyError`, so you can match either the concrete type or the base with `errors.As`. The error message resolves as `body.error` → `body.message` → HTTP status text.
| Status | Error type |
| -------------------------- | ---------------------------------------------------------------------------- |
| 401 | `*AuthenticationError` |
| 402 | `*QuotaExceededError` |
| 404 | `*TemplateNotFoundError` |
| 422 / other 4xx | `*RenderError` (422 carries field-level `Errors`) |
| 429 | `*QuotaExceededError` (when `code == "quota_exceeded"`) or `*RateLimitError` |
| 5xx | `*ServerError` |
| transport failure | `*NetworkError` |
| timeout / context deadline | `*TimeoutError` |
```go theme={null}
import (
"errors"
"github.com/pictify-io/pictify-go"
)
result, err := client.Render(ctx, opts)
if err != nil {
var authErr *pictify.AuthenticationError
var notFoundErr *pictify.TemplateNotFoundError
var rateLimitErr *pictify.RateLimitError
var quotaErr *pictify.QuotaExceededError
var renderErr *pictify.RenderError
var serverErr *pictify.ServerError
var networkErr *pictify.NetworkError
switch {
case errors.As(err, &authErr):
log.Println("invalid API key")
case errors.As(err, ¬FoundErr):
log.Println("template not found")
case errors.As(err, &rateLimitErr):
log.Printf("rate limited; retry after %ds", rateLimitErr.RetryAfter)
case errors.As(err, "aErr):
log.Println("render quota exceeded")
case errors.As(err, &renderErr):
log.Printf("render failed: %s (%v)", renderErr.Message, renderErr.Errors)
case errors.As(err, &serverErr):
log.Printf("server error: %s", serverErr.Message)
case errors.As(err, &networkErr):
log.Printf("network error: %v", networkErr.Err)
default:
log.Printf("error: %v", err)
}
}
```
Only 5xx and network failures are retried (with exponential backoff); 4xx responses are never retried.
## Context Support
All methods take a `context.Context` for cancellation and timeouts:
```go theme={null}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
img, err := client.RenderHTML(ctx, &pictify.RenderHTMLOptions{
HTML: "
Hello
",
Width: 1200,
Height: 630,
})
```
## Concurrent Requests
The client is safe for concurrent use:
```go theme={null}
var wg sync.WaitGroup
results := make(chan *pictify.ImageResult, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
img, err := client.RenderHTML(ctx, &pictify.RenderHTMLOptions{
HTML: fmt.Sprintf("
Image %d
", i),
Width: 1200,
Height: 630,
})
if err == nil {
results <- img
}
}(i)
}
wg.Wait()
close(results)
for img := range results {
fmt.Println(img.URL)
}
```
## Video Templates
Render MP4 video — or animated GIF — from video templates, generate new templates with AI, or upload a Remotion scene you wrote. Video calls use a longer per-call timeout internally (render 5m, authoring 3m); context deadlines still cancel earlier.
```go theme={null}
videos, _ := client.ListVideoTemplates(ctx)
video, err := client.RenderVideo(ctx, &pictify.RenderVideoOptions{
TemplateID: videos[0].UID,
Variables: map[string]any{"title": "Welcome, Maya!"},
Format: pictify.VideoFormatGIF, // or VideoFormatMP4 (default)
})
fmt.Println(video.URL)
generated, err := client.GenerateVideoTemplate(ctx, &pictify.GenerateVideoTemplateOptions{
Prompt: "An 8 second product launch teaser — dark, electric, type-driven",
})
// The compile gate runs BEFORE saving: invalid tsx returns a *RenderError
// with the compiler errors in .Errors, and nothing is created.
template, err := client.CreateVideoTemplate(ctx, &pictify.CreateVideoTemplateOptions{
Name: "My scene", TSX: sceneSource, DurationSeconds: 8,
})
```
## API Reference
See the [API Reference](/api-reference/overview) for full endpoint documentation.
# Node.js SDK
Source: https://docs.pictify.io/sdks/nodejs
Official Pictify SDK for Node.js
# Node.js SDK
The official Pictify SDK for Node.js provides a type-safe, promise-based interface for the Pictify API — generate images, PDFs, and GIFs from raw HTML, live URLs, and reusable templates.
## Installation
```bash npm theme={null}
npm install @pictify/sdk
```
```bash yarn theme={null}
yarn add @pictify/sdk
```
```bash pnpm theme={null}
pnpm add @pictify/sdk
```
## Quick Start
```typescript theme={null}
import { Pictify } from '@pictify/sdk';
const pictify = new Pictify({
apiKey: process.env.PICTIFY_API_KEY!,
});
// Render raw HTML to a PNG
const image = await pictify.renderHtml({
html: '
Hello World
',
width: 1200,
height: 630,
});
console.log('Image URL:', image.url);
// Render a reusable template
const result = await pictify.render({
templateId: 'XL13XACH2V',
variables: { name: 'Ada', company: 'Pictify' },
});
console.log('Image URL:', result.url);
```
## Configuration
The client is constructed with a config object. `apiKey` is required; every request is sent with an `Authorization: Bearer ` header.
```typescript theme={null}
const pictify = new Pictify({
apiKey: process.env.PICTIFY_API_KEY!, // Required: your Pictify API key
baseUrl: 'https://api.pictify.io', // Optional: API base URL (default: https://api.pictify.io)
timeout: 30000, // Optional: request timeout in ms (default: 30000)
retries: 3, // Optional: retries on 5xx / network errors (default: 3)
});
```
Keep your API key secret. Read it from an environment variable (`PICTIFY_API_KEY`) and never expose it in client-side code or public repositories.
## Render an Image from HTML
`renderHtml(options)` — `POST /image`. Returns `{ url, id, createdAt }`.
```typescript theme={null}
const image = await pictify.renderHtml({
html: '
Hello
',
css: 'div { color: blue; }', // optional — inlined into a
Hi
',
// or: url: 'https://example.com'
// or: templateId: 'XL13XACH2V', variables: { name: 'Ada' }
width: 400, // optional (default: 800)
height: 200, // optional (default: 600)
quality: 'medium', // optional: 'low' | 'medium' | 'high' (default: medium)
});
console.log(gif.url, gif.uid, gif.animationLength);
```
The source HTML/URL must contain motion (e.g. a CSS animation). Static content produces no frames and returns a render error (HTTP 422).
## Batch Rendering (async)
`renderBatch(options)` — `POST /templates/:uid/batch-render`. Returns immediately (HTTP 202) with a `batchId`; poll `getBatchResults(batchId)` (`GET /templates/batch/:batchId/results`) to track progress.
```typescript theme={null}
const job = await pictify.renderBatch({
templateId: 'XL13XACH2V',
variableSets: [
{ name: 'Ada', company: 'Pictify' },
{ name: 'Grace', company: 'Pictify' },
], // max 100 per batch
format: 'png', // optional
quality: 0.9, // optional, 0.1–1.0
concurrency: 5, // optional, 1–10 (default: 5)
// layout: 'square' or layouts: ['default', 'square'] — optional
});
// { batchId, status, totalItems }
console.log(job.batchId);
// Poll for progress.
const status = await pictify.getBatchResults(job.batchId);
console.log(status.status); // 'pending' | 'processing' | 'completed' | 'partial' | 'failed' | 'cancelled'
console.log(status.completedItems, 'of', status.totalItems);
for (const item of status.results) {
console.log(`item ${item.index}: success=${item.success}`);
}
```
**Rendered URLs are not returned by the poll endpoint.** `getBatchResults` reports per-item `{ index, success, variables }` (and `error` on failures). Final image URLs are delivered via the `render.completed` webhook — subscribe to webhooks to collect batch output.
## Templates
### Get a Template
`getTemplate(templateId)` — `GET /templates/:uid`. Unwraps the `{ template }` envelope.
```typescript theme={null}
const template = await pictify.getTemplate('XL13XACH2V');
// { uid, name, html, width, height, engine, outputFormat,
// variables: string[], variableDefinitions: [...], createdAt, ... }
console.log(template.uid, template.name);
```
### List Templates
`listTemplates(options)` — `GET /templates`. Returns `{ templates, pagination }`.
```typescript theme={null}
const { templates, pagination } = await pictify.listTemplates({
page: 1, // optional (default: 1)
limit: 20, // optional, max 100 (default: 12)
sort: 'newest', // optional: 'newest' | 'oldest' | 'name'
});
console.log(pagination.total, 'templates');
for (const t of templates) console.log(t.uid, t.name);
```
### Create a Template
`createTemplate(options)` — `POST /templates`. Variables are auto-discovered from `{{variableName}}` tokens in the HTML body.
```typescript theme={null}
const template = await pictify.createTemplate({
html: '
Hi {{firstName}}
',
name: 'Welcome Card', // optional
width: 600, // optional
height: 200, // optional
variableDefinitions: [], // optional — auto-extracted from the HTML when omitted
outputFormat: 'image', // optional: 'image' | 'pdf'
});
console.log(template.uid);
```
## Error Handling
All API errors throw a typed subclass of `PictifyError`.
```typescript theme={null}
import {
Pictify,
PictifyError,
AuthenticationError,
TemplateNotFoundError,
RateLimitError,
QuotaExceededError,
RenderError,
} from '@pictify/sdk';
try {
const result = await pictify.render({ templateId: 'XL13XACH2V' });
} catch (error) {
if (error instanceof RateLimitError) {
console.log('Rate limited — slow down');
} else if (error instanceof QuotaExceededError) {
console.log('Quota exceeded — upgrade your plan');
} else if (error instanceof RenderError) {
console.error('Render/validation failed:', error.message, error.errors);
} else if (error instanceof PictifyError) {
console.error('Pictify error:', error.code, error.message);
} else {
throw error;
}
}
```
### Error Types
| Error Class | Code | HTTP | Description |
| ----------------------- | --------------------- | ------------------- | ---------------------------------------------------------------------------- |
| `AuthenticationError` | `INVALID_API_KEY` | 401 | Invalid or missing API key |
| `QuotaExceededError` | `QUOTA_EXCEEDED` | 402 / 429 | Render quota exceeded |
| `TemplateNotFoundError` | `TEMPLATE_NOT_FOUND` | 404 | Template (or batch job) not found |
| `RenderError` | `RENDER_FAILED` | 422 (and other 4xx) | Render or input validation failed (`error.errors` holds field-level details) |
| `RateLimitError` | `RATE_LIMIT_EXCEEDED` | 429 | Too many requests |
| `ServerError` | `SERVER_ERROR` | 5xx | Server-side failure |
| `NetworkError` | `NETWORK_ERROR` | — | Network request failed |
| `TimeoutError` | `TIMEOUT` | — | Request timed out |
Only 5xx and network failures are retried (with exponential backoff); 4xx responses are never retried.
## CommonJS Support
```javascript theme={null}
const { Pictify } = require('@pictify/sdk');
const pictify = new Pictify({ apiKey: process.env.PICTIFY_API_KEY });
```
## TypeScript Support
The SDK is written in TypeScript and exports all types:
```typescript theme={null}
import {
Pictify,
ImageResult,
RenderResult,
RenderResultItem,
GifRenderResult,
BatchRenderResult,
BatchResults,
Template,
ListTemplatesResult,
ImageFormat,
GifQuality,
} from '@pictify/sdk';
```
## Next.js Integration
### Route Handler (OG image)
```typescript theme={null}
// app/api/og/route.ts
import { Pictify } from '@pictify/sdk';
import { NextRequest, NextResponse } from 'next/server';
const pictify = new Pictify({ apiKey: process.env.PICTIFY_API_KEY! });
export async function GET(req: NextRequest) {
const title = req.nextUrl.searchParams.get('title') ?? '';
const result = await pictify.render({
templateId: 'og-image-template',
variables: { title },
});
return NextResponse.redirect(result.url!);
}
```
### Express.js OG-image route
```typescript theme={null}
import express from 'express';
import { Pictify } from '@pictify/sdk';
const app = express();
const pictify = new Pictify({ apiKey: process.env.PICTIFY_API_KEY! });
app.get('/og-image', async (req, res) => {
const { title, description } = req.query as Record;
const result = await pictify.render({
templateId: 'og-image-template',
variables: { title, description },
});
res.redirect(result.url!);
});
```
## Video Templates
Render MP4 video — or animated GIF — from video templates, generate new templates with AI, or upload a Remotion scene you wrote. Video renders wait for the finished file (up to a few minutes); the SDK raises the per-call timeout automatically.
```typescript theme={null}
// Discover
const videos = await pictify.listVideoTemplates();
const vars = await pictify.getVideoTemplateVariables(videos[0].uid);
// Render — MP4 by default, or an animated GIF of the same template
const video = await pictify.renderVideo({
templateId: videos[0].uid,
variables: { title: 'Welcome, Maya!' },
format: 'gif',
});
console.log(video.url);
// Generate a template from a prompt (AI designs, writes and reviews the scene)
const generated = await pictify.generateVideoTemplate({
prompt: 'An 8 second product launch teaser — dark, electric, type-driven',
});
// Or upload a Remotion scene you wrote. The compile gate runs BEFORE saving:
// invalid tsx rejects with the compiler errors and creates nothing.
const template = await pictify.createVideoTemplate({
name: 'My scene',
tsx: sceneSource,
durationSeconds: 8,
});
```
## API Reference
See the [API Reference](/api-reference/overview) for full endpoint documentation.
# Python SDK
Source: https://docs.pictify.io/sdks/python
Official Pictify SDK for Python
# Python SDK
The official Pictify SDK for Python provides a simple, Pythonic interface for the Pictify API — generate images, PDFs, and GIFs from raw HTML, live URLs, and reusable templates. It ships sync (`Pictify`) and async (`AsyncPictify`) clients with an identical surface.
## Installation
```bash theme={null}
pip install pictify
```
## Quick Start
```python theme={null}
from pictify import Pictify
client = Pictify(api_key="your-api-key")
# Render raw HTML to a PNG
image = client.render_html(html="
Hello World
")
print(image.url)
# Render a reusable template
result = client.render("XL13XACH2V", variables={"name": "Ada", "company": "Pictify"})
print(result.url) # results[0].url
```
## Async Usage
```python theme={null}
import asyncio
from pictify import AsyncPictify
async def main():
async with AsyncPictify(api_key="your-api-key") as client:
image = await client.render_html(html="
Hello World
")
print(image.url)
asyncio.run(main())
```
## Configuration
The client is constructed with a keyword `api_key`. Every request is sent with an `Authorization: Bearer ` header.
```python theme={null}
client = Pictify(
api_key="your-api-key",
base_url="https://api.pictify.io", # optional: API base URL (default: https://api.pictify.io)
timeout=30.0, # optional: request timeout in seconds (default: 30)
max_retries=3, # optional: retries on 5xx / network errors (default: 3)
)
```
Keep your API key secret. Read it from an environment variable (e.g. `os.environ["PICTIFY_API_KEY"]`) and never expose it in client-side code or public repositories.
Both clients are context managers and expose `close()`:
```python theme={null}
with Pictify(api_key="your-api-key") as client:
...
async with AsyncPictify(api_key="your-api-key") as client:
...
```
## Render an Image from HTML
`render_html(html, *, css=None, width=None, height=None, selector=None, format=None)` — `POST /image`. Returns an `ImageResult` with `url`, `id`, and `created_at`.
```python theme={null}
image = client.render_html(
html="
Hello
",
css="div { color: blue; }", # optional — inlined into a
Hi
",
width=400, # optional (default: 800)
height=200, # optional (default: 600)
quality="medium", # optional: 'low' | 'medium' | 'high' (default: medium)
)
print(gif.url, gif.uid, gif.animation_length)
# From a template
gif = client.render_gif(template_id="XL13XACH2V", variables={"name": "Ada"})
# From a live URL
gif = client.render_gif(url="https://example.com/animated-page")
```
The source HTML/URL must contain motion (e.g. a CSS animation). Static content produces no frames and returns a render error (HTTP 422).
## Batch Rendering (async)
`render_batch(template_id, variable_sets, *, format=None, quality=None, concurrency=None, layout=None, layouts=None)` — `POST /templates/:uid/batch-render`.
Submitting returns immediately (HTTP 202) with a `batch_id`. Poll `get_batch_results` for progress.
```python theme={null}
job = client.render_batch(
"XL13XACH2V",
variable_sets=[
{"name": "Ada", "company": "Pictify"},
{"name": "Grace", "company": "Pictify"},
], # max 100 per batch
format="png", # optional
quality=0.9, # optional, 0.1-1.0
concurrency=5, # optional, 1-10 (default: 5)
)
print(job.batch_id, job.status, job.total_items)
```
`get_batch_results(batch_id)` — `GET /templates/batch/:batchId/results`.
```python theme={null}
status = client.get_batch_results(job.batch_id)
print(status.status, status.completed_items, "/", status.total_items)
for item in status.results:
print(item.index, item.success, item.variables) # no URLs (see webhook)
```
**Rendered URLs are not returned by the poll endpoint.** Per-item records carry `{index, success, variables}` (and `error` on failures). Final image URLs are delivered via the `render.completed` webhook — subscribe to webhooks to collect batch output.
## Templates
### Get a Template
`get_template(template_id)` — `GET /templates/:uid`.
```python theme={null}
template = client.get_template("XL13XACH2V")
print(template.uid, template.name)
print([v.name for v in (template.variable_definitions or [])])
```
### List Templates
`list_templates(*, page=None, limit=None, sort=None)` — `GET /templates`. Returns a `ListTemplatesResult` with `templates` and `pagination`.
```python theme={null}
result = client.list_templates(page=1, limit=20, sort="newest")
for t in result.templates:
print(t.uid, t.name)
print(result.pagination.total, result.pagination.has_next)
```
### Create a Template
`create_template(html, *, name=None, width=None, height=None, variable_definitions=None, output_format=None)` — `POST /templates`. Variables are auto-discovered from `{{variableName}}` tokens.
```python theme={null}
template = client.create_template(
html="
Hi {{first_name}}
",
name="Welcome Card",
width=600,
height=200,
output_format="image", # optional: 'image' | 'pdf'
)
print(template.uid)
```
## Error Handling
```python theme={null}
from pictify import (
Pictify,
PictifyError,
AuthenticationError,
TemplateNotFoundError,
RateLimitError,
QuotaExceededError,
RenderError,
ServerError,
)
client = Pictify(api_key="your-api-key")
try:
result = client.render("XL13XACH2V", variables={"name": "Ada"})
except AuthenticationError:
print("Invalid API key")
except TemplateNotFoundError:
print("Template not found")
except RateLimitError as e:
print(f"Rate limited. Retry after: {e.retry_after}s")
except QuotaExceededError:
print("Render quota exceeded")
except RenderError as e:
print(f"Render/validation failed: {e.message}; field errors: {e.errors}")
except ServerError as e:
print(f"Server error: {e.message}")
except PictifyError as e:
print(f"Error: {e.message}")
```
Status → error mapping: `401 → AuthenticationError`, `402 → QuotaExceededError`, `404 → TemplateNotFoundError`, `422 → RenderError` (with field-level `errors`), `429 → QuotaExceededError` when `code == "quota_exceeded"` else `RateLimitError`, other 4xx → `RenderError`, `5xx → ServerError`. Only 5xx and network errors are retried.
## Type Hints
The SDK ships full type hints and Pydantic result models:
```python theme={null}
from pictify import (
Pictify,
AsyncPictify,
ImageResult,
RenderResult,
RenderResultItem,
GifRenderResult,
BatchRenderResult,
BatchResults,
Template,
ListTemplatesResult,
ImageFormat,
GifQuality,
)
```
## Django Integration
```python theme={null}
# views.py
from django.conf import settings
from django.http import HttpResponseRedirect
from pictify import Pictify
client = Pictify(api_key=settings.PICTIFY_API_KEY)
def og_image(request, slug):
post = Post.objects.get(slug=slug)
result = client.render(
"og-image-template",
variables={
"title": post.title,
"description": post.excerpt,
"author": post.author.name,
},
)
return HttpResponseRedirect(result.url)
```
## Video Templates
Render MP4 video — or animated GIF — from video templates, generate new templates with AI, or upload a Remotion scene you wrote. Available on both `Pictify` and `AsyncPictify`; renders wait for the finished file, with the per-call timeout raised automatically.
```python theme={null}
videos = client.list_video_templates()
vars = client.get_video_template_variables(videos[0].uid)
# MP4 by default, or an animated GIF of the same template
video = client.render_video(videos[0].uid, variables={"title": "Welcome, Maya!"}, format="gif")
print(video.url)
# Generate a template from a prompt (AI designs, writes and reviews the scene)
generated = client.generate_video_template("An 8 second product launch teaser")
# Or upload a Remotion scene you wrote. The compile gate runs BEFORE saving:
# invalid tsx raises with the compiler errors and creates nothing.
template = client.create_video_template("My scene", scene_source, duration_seconds=8)
```
## API Reference
See the [API Reference](/api-reference/overview) for full endpoint documentation.
# Ruby SDK
Source: https://docs.pictify.io/sdks/ruby
Official Pictify SDK for Ruby
# Ruby SDK
The official Pictify SDK for Ruby provides an idiomatic Ruby interface for the Pictify API — generate images, PDFs, and GIFs from raw HTML, live URLs, and reusable templates.
## Installation
Add to your Gemfile:
```ruby theme={null}
gem "pictify"
```
Then run:
```bash theme={null}
bundle install
```
Or install directly:
```bash theme={null}
gem install pictify
```
## Quick Start
```ruby theme={null}
require "pictify"
client = Pictify::Client.new(api_key: "your-api-key")
# Render raw HTML to a PNG
image = client.render_html(
html: "
Hello World
",
width: 1200,
height: 630
)
puts image.url
# Render a template
result = client.render(
template_id: "XL13XACH2V",
variables: { name: "Ada", company: "Pictify" }
)
puts result.url
```
## Configuration
The client is constructed with a keyword `api_key`. Every request is sent with an `Authorization: Bearer ` header.
```ruby theme={null}
client = Pictify::Client.new(
api_key: "your-api-key",
base_url: "https://api.pictify.io", # optional: API base URL (default: https://api.pictify.io)
timeout: 30, # optional: request timeout in seconds (default: 30)
max_retries: 3 # optional: retries on 5xx / network errors (default: 3)
)
```
Keep your API key secret. Read it from an environment variable (e.g. `ENV["PICTIFY_API_KEY"]`) and never expose it in client-side code or public repositories.
## Rendering Images
### From HTML — `POST /image`
Returns an `ImageResult` with `url`, `id`, and `created_at`.
```ruby theme={null}
image = client.render_html(
html: "
Hello
",
css: "div { color: blue; }", # injected as a
Hi
",
width: 400, # default 800
height: 200, # default 600
quality: :medium # :low, :medium, :high
)
puts gif.url
puts gif.uid
puts gif.animation_length
# From a template
gif = client.render_gif(template_id: "XL13XACH2V", variables: { name: "Ada" })
# From a live URL
gif = client.render_gif(url: "https://example.com/animated-page")
```
Static content produces no frames and returns a render error (HTTP 422).
## Batch Rendering (async) — `POST /templates/:uid/batch-render`
Batch rendering is asynchronous. Submitting returns a `batch_id` immediately (HTTP 202); poll `get_batch_results` to track progress.
```ruby theme={null}
job = client.render_batch(
template_id: "XL13XACH2V",
variable_sets: [
{ name: "Card 1", company: "X" },
{ name: "Card 2", company: "Y" }
],
format: :png,
quality: 0.9, # optional
concurrency: 5, # optional, 1–10
layouts: ["default", "twitter-post"] # optional
)
puts job.batch_id
puts job.status # "pending"
puts job.total_items
# Poll for progress
results = client.get_batch_results(job.batch_id)
puts "status: #{results.status} (#{results.progress}%)"
puts "completed: #{results.completed_items} / #{results.total_items}"
results.results.each do |item|
puts "Item #{item.index}: success=#{item.success?} vars=#{item.variables}"
end
```
The poll endpoint reports per-item `index`, `success`, and `variables` but **not** rendered URLs — URLs are delivered via the `render.completed` webhook.
## Template Management
```ruby theme={null}
# Get a template by UID — GET /templates/:uid
template = client.get_template("XL13XACH2V")
puts "Template: #{template.name} (#{template.uid})"
template.variable_definitions.each do |var|
puts " - #{var.name} (#{var.type})"
end
# List templates — GET /templates
result = client.list_templates(page: 1, limit: 20, sort: :newest)
result.templates.each { |t| puts "#{t.uid}: #{t.name}" }
puts "total: #{result.pagination.total}"
# Create a template from HTML — POST /templates
# Variables are auto-discovered from {{variableName}} tokens.
template = client.create_template(
html: "
Hi {{firstName}}
",
name: "Welcome Card",
width: 600,
height: 200,
output_format: "image" # "image" | "pdf"
)
puts template.uid
```
## Error Handling
```ruby theme={null}
begin
result = client.render(template_id: "XL13XACH2V", variables: { name: "Ada" })
rescue Pictify::AuthenticationError
puts "Invalid API key"
rescue Pictify::TemplateNotFoundError => e
puts "Template not found: #{e.message}"
rescue Pictify::QuotaExceededError
puts "Render quota exceeded"
rescue Pictify::RateLimitError => e
puts "Rate limited. Retry after: #{e.retry_after}s"
rescue Pictify::RenderError => e
puts "Render/validation failed: #{e.message}"
puts e.errors # field-level validation errors when present (422)
rescue Pictify::ServerError => e
puts "Server error: #{e.message}"
rescue Pictify::NetworkError => e
puts "Network error: #{e.message}"
rescue Pictify::TimeoutError
puts "Request timed out"
rescue Pictify::Error => e
puts "Error: #{e.message}"
end
```
| Status | Error class |
| ---------------------- | ----------------------------------------- |
| 401 | `Pictify::AuthenticationError` |
| 402 | `Pictify::QuotaExceededError` |
| 404 | `Pictify::TemplateNotFoundError` |
| 422 | `Pictify::RenderError` (carries `errors`) |
| 429 (`quota_exceeded`) | `Pictify::QuotaExceededError` |
| 429 (other) | `Pictify::RateLimitError` |
| other 4xx | `Pictify::RenderError` |
| 5xx | `Pictify::ServerError` |
Only 5xx responses and network failures are retried (with exponential backoff); 4xx responses (including 429) are never retried.
## Framework Example — Rails
```ruby theme={null}
class OgImagesController < ApplicationController
def show
client = Pictify::Client.new(api_key: ENV["PICTIFY_API_KEY"])
result = client.render(
template_id: "og-image-template",
variables: { title: params[:title] }
)
redirect_to result.url, allow_other_host: true
end
end
```
## Video Templates
Render MP4 video — or animated GIF — from video templates, generate new templates with AI, or upload a Remotion scene you wrote. Renders wait for the finished file; the SDK raises the per-call timeout automatically (render 300s, authoring 180s).
```ruby theme={null}
videos = client.list_video_templates
vars = client.get_video_template_variables(videos.first.uid)
# MP4 by default, or an animated GIF of the same template
video = client.render_video(videos.first.uid, variables: { title: "Welcome, Maya!" }, format: "gif")
puts video.url
# Generate a template from a prompt (AI designs, writes and reviews the scene)
generated = client.generate_video_template(prompt: "An 8 second product launch teaser")
# Or upload a Remotion scene you wrote. The compile gate runs BEFORE saving:
# invalid tsx raises Pictify::RenderError with the compiler errors and creates nothing.
template = client.create_video_template(name: "My scene", tsx: scene_source, duration_seconds: 8)
```
## API Reference
See the [API Reference](/api-reference/overview) for full endpoint documentation.
# API Key Security
Source: https://docs.pictify.io/security/api-keys
Best practices for managing and securing API keys
# API Key Security
API keys authenticate your requests to the Pictify API. Proper key management is essential for security.
## Key Format
An API key is a single 64-character hex string with full access to your team's resources. There are no key types, prefixes, or sandbox keys — every key is live, and every render it makes consumes your plan's monthly credits.
## Creating API Keys
### Dashboard
1. Go to **Settings** > **API Keys**
2. Click **Create Key**
3. Name your key (e.g., "Production Server", "CI/CD")
4. Copy the key immediately - it's only shown once
### Key Properties
Each key includes:
* **Secret** - The key value itself (a 64-character hex string)
* **Name** - Your description
* **Created** - Creation timestamp
* **Last Used** - Last API call timestamp
## Storing Keys Securely
### Environment Variables
The recommended approach for most applications:
```bash theme={null}
# .env (never commit this file)
PICTIFY_API_KEY=your-64-character-api-key
```
```typescript theme={null}
// Load from environment
const pictify = new Pictify({ apiKey: process.env.PICTIFY_API_KEY });
```
Never commit API keys to version control. Add `.env` to your `.gitignore`.
### Secrets Managers
For production environments, use a secrets manager:
#### AWS Secrets Manager
```typescript theme={null}
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManager();
const response = await client.getSecretValue({ SecretId: 'pictify/api-key' });
const apiKey = response.SecretString;
const pictify = new Pictify({ apiKey });
```
#### Google Secret Manager
```typescript theme={null}
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
const client = new SecretManagerServiceClient();
const [version] = await client.accessSecretVersion({
name: 'projects/my-project/secrets/pictify-api-key/versions/latest'
});
const apiKey = version.payload.data.toString();
```
#### HashiCorp Vault
```typescript theme={null}
import Vault from 'node-vault';
const vault = Vault({ endpoint: process.env.VAULT_ADDR });
const result = await vault.read('secret/data/pictify');
const apiKey = result.data.data.api_key;
```
### Kubernetes Secrets
```yaml theme={null}
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: pictify-credentials
type: Opaque
stringData:
api-key: your-64-character-api-key
```
```yaml theme={null}
# deployment.yaml
env:
- name: PICTIFY_API_KEY
valueFrom:
secretKeyRef:
name: pictify-credentials
key: api-key
```
## Key Rotation
Regularly rotate API keys to limit exposure from potential leaks.
### Rotation Process
1. **Create new key** - Generate a new API key in the dashboard
2. **Update applications** - Deploy the new key to all services
3. **Verify** - Confirm all services are using the new key
4. **Revoke old key** - Delete the old key from the dashboard
### Zero-Downtime Rotation
For production systems, use overlapping validity:
```typescript theme={null}
// During rotation, try both keys
const keys = [
process.env.PICTIFY_API_KEY_NEW,
process.env.PICTIFY_API_KEY_OLD
].filter(Boolean);
async function makeRequest(options) {
for (const key of keys) {
try {
const pictify = new Pictify({ apiKey: key });
return await pictify.renderHtml(options);
} catch (error) {
if (error.status === 401 && keys.indexOf(key) < keys.length - 1) {
continue; // Try next key
}
throw error;
}
}
}
```
## Access Control
### Principle of Least Privilege
Create separate keys for different purposes:
| Key Name | Purpose | Access Level |
| -------------------- | ------------------- | ------------ |
| `prod-api-server` | Production API | Full access |
| `staging-server` | Staging environment | Test key |
| `ci-cd-pipeline` | Automated tests | Test key |
| `analytics-readonly` | Metrics collection | Read-only |
### Team Access
* **Limit who can create keys** - Only admins should create production keys
* **Audit key usage** - Monitor which keys are being used
* **Remove departed employees** - Revoke keys when team members leave
## Monitoring & Auditing
### Track Key Usage
Monitor your API usage in the dashboard:
* Requests per key
* Error rates
* Last used timestamp
* Geographic distribution
### Set Up Alerts
Configure alerts for suspicious activity:
* Sudden spike in requests
* Requests from unexpected locations
* High error rates
* Usage outside business hours
### Audit Logs
Review audit logs regularly:
```json theme={null}
{
"timestamp": "2026-01-29T10:30:00Z",
"action": "api.request",
"keyId": "key_abc123",
"endpoint": "/image",
"method": "POST",
"status": 200,
"ip": "203.0.113.50",
"userAgent": "pictify-node/1.0.0"
}
```
## Handling Compromised Keys
If you suspect a key has been compromised:
### Immediate Actions
1. **Revoke immediately** - Delete the key in the dashboard
2. **Create new key** - Generate a replacement
3. **Update services** - Deploy the new key
4. **Review logs** - Check for unauthorized usage
### Investigation
1. **Identify scope** - What data could have been accessed?
2. **Check usage** - Review API logs for suspicious activity
3. **Determine source** - How was the key exposed?
4. **Prevent recurrence** - Implement safeguards
## Security Checklist
* [ ] API keys stored in environment variables or secrets manager
* [ ] `.env` files excluded from version control
* [ ] Separate keys for production and development
* [ ] Keys rotated regularly (at least annually)
* [ ] Unused keys revoked
* [ ] API usage monitored for anomalies
* [ ] Access to key creation restricted
* [ ] Incident response plan documented
## Common Mistakes
### Hardcoding Keys
```typescript theme={null}
// ❌ Never do this
const pictify = new Pictify({ apiKey: 'a3f8c2...' }); // never hardcode
// ✅ Use environment variables
const pictify = new Pictify({ apiKey: process.env.PICTIFY_API_KEY });
```
### Committing Keys
```bash theme={null}
# ❌ This exposes your key
git commit -m "Add API integration"
# ✅ Use .gitignore
echo ".env" >> .gitignore
```
### Sharing Keys in Chat
```
❌ "Here's the API key: a3f8c2d914..."
✅ "I've added the key to the team secrets manager"
```
### Using Production Keys in Development
```bash theme={null}
# ❌ Development with production key
PICTIFY_API_KEY=YOUR_PRODUCTION_KEY npm run dev
PICTIFY_API_KEY=YOUR_DEVELOPMENT_KEY npm run dev
```
# SSRF Protection
Source: https://docs.pictify.io/security/ssrf-protection
How Pictify protects against Server-Side Request Forgery attacks
# SSRF Protection
Server-Side Request Forgery (SSRF) is a security vulnerability where an attacker tricks a server into making requests to unintended locations. Pictify implements multiple layers of protection when rendering URLs or fetching data.
## What is SSRF?
When Pictify renders a URL or fetches data for bindings, it makes HTTP requests on your behalf. Without protection, an attacker could potentially:
* Access internal services (e.g., `http://localhost:8080/admin`)
* Probe private networks (e.g., `http://192.168.1.1`)
* Access cloud metadata (e.g., `http://169.254.169.254`)
* Scan internal ports
## Pictify's Protections
### 1. URL Validation
All URLs are validated before requests are made:
```
✅ https://example.com/page
✅ https://api.github.com/repos/owner/repo
❌ http://localhost/admin
❌ http://127.0.0.1:8080
❌ http://192.168.1.1/internal
❌ http://169.254.169.254/metadata
❌ file:///etc/passwd
```
### 2. Blocked IP Ranges
Requests to these IP ranges are blocked:
| Range | Description |
| ---------------- | ------------------------- |
| `127.0.0.0/8` | Localhost |
| `10.0.0.0/8` | Private network (Class A) |
| `172.16.0.0/12` | Private network (Class B) |
| `192.168.0.0/16` | Private network (Class C) |
| `169.254.0.0/16` | Link-local (AWS metadata) |
| `0.0.0.0/8` | Current network |
| `::1` | IPv6 localhost |
| `fc00::/7` | IPv6 private |
### 3. DNS Resolution Protection
Pictify resolves DNS before making requests and blocks if the resolved IP is in a blocked range:
```
example.internal.com → 192.168.1.100 → BLOCKED
```
This prevents DNS rebinding attacks where a domain initially resolves to a public IP but later resolves to a private IP.
### 4. Protocol Restrictions
Only HTTP and HTTPS protocols are allowed:
```
✅ https://example.com
✅ http://example.com (upgraded to HTTPS)
❌ file:///etc/passwd
❌ ftp://server.com/file
❌ gopher://server.com
```
### 5. Redirect Following
Redirects are validated at each step:
```
https://example.com/page
→ 301 to https://example.com/new-page ✅
→ 301 to http://localhost/admin ❌ BLOCKED
```
## Using URL Features Safely
### Screenshot from URL
When rendering screenshots from URLs, Pictify validates the target:
```typescript theme={null}
// ✅ Safe - public URL
const image = await pictify.renderUrl({
url: 'https://example.com',
width: 1200,
height: 630
});
// ❌ Blocked - private IP
const blocked = await pictify.renderUrl({
url: 'http://192.168.1.1/admin', // Will fail
width: 1200,
height: 630
});
```
### Bindings with External Data
Bindings are created via the `POST /bindings` REST endpoint (the SDKs don't wrap bindings). The same SSRF rules apply to the binding's data URL:
```bash theme={null}
# ✅ Safe - public API
curl -X POST https://api.pictify.io/bindings \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateUid": "tmpl_abc123",
"url": "https://api.github.com/repos/your/repo",
"refreshPolicy": { "type": "ttl", "ttl": 3600 }
}'
```
```bash theme={null}
# ❌ Blocked - internal service (request will fail)
curl -X POST https://api.pictify.io/bindings \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateUid": "tmpl_abc123",
"url": "http://internal-api.local/metrics",
"refreshPolicy": { "type": "ttl", "ttl": 3600 }
}'
```
### HTML with External Resources
External resources in HTML are also validated:
```html theme={null}
```
## Error Handling
When SSRF protection blocks a request, you'll receive a clear error:
```json theme={null}
{
"type": "https://docs.pictify.io/errors/blocked-url",
"title": "URL Blocked",
"status": 422,
"detail": "The requested URL points to a blocked IP range (private network).",
"instance": "/image"
}
```
## Best Practices for Your Application
### Validate User Input
If your application passes user-provided URLs to Pictify, validate them first:
```typescript theme={null}
import { URL } from 'url';
function isValidPublicUrl(urlString: string): boolean {
try {
const url = new URL(urlString);
// Only allow HTTP(S)
if (!['http:', 'https:'].includes(url.protocol)) {
return false;
}
// Block localhost
if (['localhost', '127.0.0.1', '::1'].includes(url.hostname)) {
return false;
}
// Block private IP ranges (basic check)
const hostname = url.hostname;
if (
hostname.startsWith('10.') ||
hostname.startsWith('192.168.') ||
hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)
) {
return false;
}
return true;
} catch {
return false;
}
}
// Use before passing to Pictify
if (isValidPublicUrl(userProvidedUrl)) {
const image = await pictify.renderUrl({
url: userProvidedUrl,
width: 1200,
height: 630
});
}
```
### Use Allowlists
For user-provided URLs, consider using an allowlist:
```typescript theme={null}
const ALLOWED_DOMAINS = [
'example.com',
'cdn.example.com',
'images.unsplash.com'
];
function isAllowedDomain(urlString: string): boolean {
try {
const url = new URL(urlString);
return ALLOWED_DOMAINS.some(domain =>
url.hostname === domain || url.hostname.endsWith('.' + domain)
);
} catch {
return false;
}
}
```
### Log Suspicious Activity
Monitor for potential SSRF attempts:
```typescript theme={null}
app.post('/api/screenshot', async (req, res) => {
const { url } = req.body;
// Log the URL being requested
logger.info('Screenshot requested', { url, userId: req.user.id });
try {
const image = await pictify.renderUrl({ url, width: 1200, height: 630 });
res.json({ url: image.url });
} catch (error) {
if (error.type === 'https://docs.pictify.io/errors/blocked-url') {
// Log potential SSRF attempt
logger.warn('SSRF attempt blocked', {
url,
userId: req.user.id,
ip: req.ip
});
}
throw error;
}
});
```
## Frequently Asked Questions
### Can I render localhost URLs?
No. Localhost and private network URLs are blocked for security. Use public URLs or upload your HTML content directly.
### Can I render internal company sites?
Internal sites (private IPs, internal DNS) cannot be rendered. If you need to render internal content:
1. Make the content publicly accessible (with authentication if needed)
2. Use HTML directly instead of URL rendering
### Why was my URL blocked?
Common reasons:
* URL resolves to a private IP address
* URL uses a non-HTTP(S) protocol
* URL redirects to a blocked location
* Domain is on a blocklist
### Can I whitelist specific internal URLs?
For Enterprise customers, contact support to discuss custom URL allowlists for specific use cases.
# Webhook Verification
Source: https://docs.pictify.io/security/webhook-verification
Verify webhook signatures to ensure authenticity
# Webhook Verification
Webhook signature verification ensures that incoming webhooks are genuinely from Pictify and haven't been tampered with.
Always verify webhook signatures in production. Never skip verification, even for testing.
## How It Works
1. When you create a webhook subscription, Pictify generates a unique signing secret
2. For each webhook delivery, Pictify creates a signature using HMAC-SHA256
3. Your server verifies the signature before processing the webhook
## Signature Format
The signature is sent in the `X-Pictify-Signature` header:
```
X-Pictify-Signature: t=1706515260,v1=abc123...
```
| Component | Description |
| --------- | ------------------------------------------------ |
| `t` | Unix timestamp when the webhook was sent |
| `v1` | HMAC-SHA256 signature of `{timestamp}.{payload}` |
## Verification Algorithm
1. **Extract components** - Parse the timestamp (`t`) and signature (`v1`) from the header
2. **Check timestamp** - Reject if older than 5 minutes (replay protection)
3. **Compute signature** - Calculate `HMAC-SHA256(secret, "{timestamp}.{payload}")`
4. **Compare** - Use constant-time comparison to compare signatures
## Implementation Examples
### Node.js
```typescript theme={null}
import crypto from 'crypto';
interface VerificationResult {
valid: boolean;
error?: string;
}
export function verifyWebhookSignature(
payload: string,
signatureHeader: string,
secret: string
): VerificationResult {
// Parse the signature header
const parts: Record = {};
for (const pair of signatureHeader.split(',')) {
const [key, value] = pair.split('=');
parts[key] = value;
}
const timestamp = parseInt(parts.t, 10);
const providedSignature = parts.v1;
// Check timestamp (5 minute tolerance)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - timestamp) > 300) {
return { valid: false, error: 'Timestamp too old' };
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
const valid = crypto.timingSafeEqual(
Buffer.from(providedSignature),
Buffer.from(expectedSignature)
);
return { valid };
}
```
### Python
```python theme={null}
import hmac
import hashlib
import time
from typing import Tuple
def verify_webhook_signature(
payload: bytes,
signature_header: str,
secret: str
) -> Tuple[bool, str | None]:
"""
Verify a Pictify webhook signature.
Returns:
Tuple of (is_valid, error_message)
"""
# Parse the signature header
parts = dict(pair.split('=') for pair in signature_header.split(','))
timestamp = int(parts.get('t', 0))
provided_signature = parts.get('v1', '')
# Check timestamp (5 minute tolerance)
current_time = int(time.time())
if abs(current_time - timestamp) > 300:
return False, "Timestamp too old"
# Compute expected signature
signed_payload = f"{timestamp}.{payload.decode()}"
expected_signature = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# Constant-time comparison
valid = hmac.compare_digest(provided_signature, expected_signature)
return valid, None if valid else "Invalid signature"
```
### Go
```go theme={null}
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"math"
"strconv"
"strings"
"time"
)
type VerificationError struct {
Message string
}
func (e *VerificationError) Error() string {
return e.Message
}
func VerifyWebhookSignature(payload, signatureHeader, secret string) error {
// Parse the signature header
parts := make(map[string]string)
for _, pair := range strings.Split(signatureHeader, ",") {
kv := strings.SplitN(pair, "=", 2)
if len(kv) == 2 {
parts[kv[0]] = kv[1]
}
}
timestamp, err := strconv.ParseInt(parts["t"], 10, 64)
if err != nil {
return &VerificationError{"Invalid timestamp"}
}
providedSignature := parts["v1"]
// Check timestamp (5 minute tolerance)
currentTime := time.Now().Unix()
if math.Abs(float64(currentTime-timestamp)) > 300 {
return &VerificationError{"Timestamp too old"}
}
// Compute expected signature
signedPayload := fmt.Sprintf("%d.%s", timestamp, payload)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
expectedSignature := hex.EncodeToString(mac.Sum(nil))
// Constant-time comparison
if !hmac.Equal([]byte(providedSignature), []byte(expectedSignature)) {
return &VerificationError{"Invalid signature"}
}
return nil
}
```
### Ruby
```ruby theme={null}
require 'openssl'
require 'rack/utils'
module Pictify
class WebhookVerifier
TOLERANCE = 300 # 5 minutes
def self.verify(payload:, signature_header:, secret:)
parts = signature_header.split(',').map { |p| p.split('=') }.to_h
timestamp = parts['t'].to_i
provided_signature = parts['v1']
# Check timestamp
if (Time.now.to_i - timestamp).abs > TOLERANCE
return { valid: false, error: 'Timestamp too old' }
end
# Compute expected signature
signed_payload = "#{timestamp}.#{payload}"
expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret, signed_payload)
# Constant-time comparison
valid = Rack::Utils.secure_compare(provided_signature, expected_signature)
{ valid: valid, error: valid ? nil : 'Invalid signature' }
end
end
end
```
## Framework Integration
### Express.js
```typescript theme={null}
import express from 'express';
import { verifyWebhookSignature } from './webhook-utils';
const app = express();
// Important: Use raw body parser for webhook routes
app.post(
'/webhooks/pictify',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-pictify-signature'] as string;
const payload = req.body.toString();
const { valid, error } = verifyWebhookSignature(
payload,
signature,
process.env.PICTIFY_WEBHOOK_SECRET!
);
if (!valid) {
console.error('Webhook verification failed:', error);
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
// Process the webhook
switch (event.event) {
case 'render.completed':
handleRenderCompleted(event.data);
break;
case 'render.failed':
handleRenderFailed(event.data);
break;
}
res.status(200).send('OK');
}
);
```
### FastAPI
```python theme={null}
from fastapi import FastAPI, Request, HTTPException, Header
app = FastAPI()
@app.post("/webhooks/pictify")
async def handle_webhook(
request: Request,
x_pictify_signature: str = Header(...)
):
payload = await request.body()
valid, error = verify_webhook_signature(
payload,
x_pictify_signature,
WEBHOOK_SECRET
)
if not valid:
raise HTTPException(status_code=401, detail=error)
event = await request.json()
# Process the webhook
if event['event'] == 'render.completed':
await handle_render_completed(event['data'])
elif event['event'] == 'render.failed':
await handle_render_failed(event['data'])
return {"status": "ok"}
```
### Rails
```ruby theme={null}
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def pictify
result = Pictify::WebhookVerifier.verify(
payload: request.raw_post,
signature_header: request.headers['X-Pictify-Signature'],
secret: ENV['PICTIFY_WEBHOOK_SECRET']
)
unless result[:valid]
Rails.logger.error "Webhook verification failed: #{result[:error]}"
return head :unauthorized
end
event = JSON.parse(request.raw_post)
case event['event']
when 'render.completed'
HandleRenderCompletedJob.perform_later(event['data'])
when 'render.failed'
HandleRenderFailedJob.perform_later(event['data'])
end
head :ok
end
end
```
## Security Best Practices
### 1. Store Secrets Securely
```bash theme={null}
# Use environment variables
export PICTIFY_WEBHOOK_SECRET=whsec_abc123...
# Or use a secrets manager
aws secretsmanager get-secret-value --secret-id pictify/webhook-secret
```
### 2. Use Constant-Time Comparison
Always use constant-time comparison to prevent timing attacks:
```typescript theme={null}
// ✅ Good - constant time
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))
// ❌ Bad - vulnerable to timing attacks
a === b
```
### 3. Enforce Timestamp Checks
Replay protection prevents attackers from resending old webhooks:
```typescript theme={null}
// Reject webhooks older than 5 minutes
const MAX_AGE = 300; // 5 minutes in seconds
if (Math.abs(Date.now() / 1000 - timestamp) > MAX_AGE) {
throw new Error('Webhook too old');
}
```
### 4. Use HTTPS Only
Always use HTTPS for your webhook endpoint:
```
✅ https://api.yoursite.com/webhooks/pictify
❌ http://api.yoursite.com/webhooks/pictify
```
### 5. Log Verification Failures
Monitor for potential attacks:
```typescript theme={null}
if (!valid) {
logger.warn('Webhook verification failed', {
ip: req.ip,
signature: signature.substring(0, 20) + '...',
timestamp,
error
});
}
```
## Troubleshooting
### "Invalid signature" Error
1. **Check secret** - Ensure you're using the correct webhook secret
2. **Raw body** - Make sure you're using the raw request body, not parsed JSON
3. **Encoding** - The payload must be UTF-8 encoded
### "Timestamp too old" Error
1. **Clock sync** - Ensure your server's clock is synchronized (NTP)
2. **Processing delay** - If processing takes too long, the webhook may expire
3. **Retry queue** - Pictify retries failed webhooks, which may be older
### Common Mistakes
```typescript theme={null}
// ❌ Wrong - JSON.stringify may change the payload
const payload = JSON.stringify(req.body);
// ✅ Correct - use raw body
const payload = req.body.toString();
```
```python theme={null}
# ❌ Wrong - using parsed JSON
payload = json.dumps(request.json)
# ✅ Correct - use raw body
payload = request.get_data()
```