# 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}

{{titleCase title}}

{{currency price 'USD'}}

{{date publishedAt 'MMM D, YYYY'}}

{{#each items}}
  • {{@index}}. {{this.name}} — {{currency this.price 'USD'}}
  • {{/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}

    {{title}}

    By {{author}}

    ``` ### Conditional Rendering ```html theme={null}
    Premium
    ``` ### Expressions ```html theme={null}

    Total: {{currency(price * quantity, 'USD')}}

    ``` 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}}
  • {{@index}}. {{this.name}} — {{currency this.price 'USD'}}
  • {{else}}
  • No 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: `

    Hello, Pictify!

    `, width: 1200, height: 630 }); console.log('Image URL:', image.url); ``` ```python Python theme={null} from pictify import Pictify client = Pictify(api_key="your-api-key") image = client.render_html( html="""

    Hello, Pictify!

    """, width=1200, height=630 ) print("Image URL:", image.url) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/pictify-io/pictify-go" ) func main() { client := pictify.NewClient("your-api-key") image, err := client.RenderHTML(context.Background(), &pictify.RenderHTMLOptions{ HTML: `

    Hello, Pictify!

    `, Width: 1200, Height: 630, }) if err != nil { panic(err) } fmt.Println("Image URL:", image.URL) } ``` ```ruby Ruby theme={null} require 'pictify' client = Pictify::Client.new(api_key: 'your-api-key') image = client.render_html( html: <<~HTML,

    Hello, Pictify!

    HTML width: 1200, height: 630 ) puts "Image URL: #{image.url}" ```
    ### Using cURL ```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, Pictify!

    ", "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: `
    Hello
    `, CSS: ".card { padding: 40px; color: #0ea5e9; }", // injected as ` + `
    Hi
    `, 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() ```