Templates & Rendering
Using CURL with RenderStack
RenderStack provides a REST API for rendering images from templates programmatically. All API endpoints require authentication via API key.
Base URL#
https://your-app.replit.app/api/v1
Authentication#
All API requests must include an API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
API keys can be created and managed from the API Keys page in the dashboard. See the API Keys documentation for more details.
Error Format#
All errors follow a consistent format:
{
"type": "https://renderstack.dev/errors/error-type",
"title": "Error Title",
"status": 400,
"detail": "A human-readable description of the error"
}
Error Types#
| Status | Type | Description |
|---|---|---|
| 400 | validation-error | Invalid or missing request parameters |
| 401 | authentication-error | Invalid, missing, or revoked API key |
| 403 | forbidden | Key type mismatch or origin not allowed |
| 404 | not-found | Template or resource not found |
| 414 | uri-too-long | URL exceeds 8,192 character limit (GET only) |
| 429 | rate-limited | Rate limit exceeded (GET only) |
| 500 | server-error | Internal server error during processing |
Endpoints#
Render Image (Binary)#
Renders a template and returns the image as binary data (raw image file).
POST /api/v1/renders/sync
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
template | string | Yes | Template ID or slug |
layers | object | No | Dynamic layer overrides (see below) |
format | string | No | Output format: png, jpeg, or pdf (default: png) |
quality | number | No | JPEG quality 1-100 (default: 90, JPEG only) |
width | number | No | Override output width in pixels |
height | number | No | Override output height in pixels |
sizes | array | No | Array of { width, height } for multi-page PDF (max 50 entries, PDF only) |
useAws | boolean | No | Upload the rendered output to your configured AWS S3 bucket |
filename | string | No | Custom filename for the S3 object (e.g. "my-render.png"). Ignored if useAws is not true |
Example Request:
curl -X POST https://your-app.replit.app/api/v1/renders/sync \
-H "Authorization: Bearer rs_abc123..." \
-H "Content-Type: application/json" \
-d '{
"template": "social-card",
"layers": {
"title": { "text": "Welcome to Our Event" },
"subtitle": { "text": "Join us for an amazing experience" },
"background_image": { "src": "https://example.com/bg.jpg" },
"date_text": { "text": "March 15, 2026" }
},
"format": "png"
}' \
--output social-card.png
Success Response:
- Status: 200
- Content-Type:
image/png,image/jpeg, orapplication/pdf - Body: Raw binary data (image or PDF document)
Response Headers:
| Header | Description |
|---|---|
X-Render-Id | Unique ID of the render record |
X-Render-Duration-Ms | Time taken to render in milliseconds |
X-Render-Warnings | JSON array of warnings (if any) |
X-Render-S3-Url | S3 object URL (only when useAws: true and upload succeeds) |
X-Render-S3-Key | S3 object key (only when useAws: true and upload succeeds) |
Render Image (JSON)#
Renders a template and returns the image as a base64-encoded JSON response.
POST /api/v1/renders/json
Request Body: Same as the binary render endpoint above.
Example Request:
curl -X POST https://your-app.replit.app/api/v1/renders/json \
-H "Authorization: Bearer rs_abc123..." \
-H "Content-Type: application/json" \
-d '{
"template": "social-card",
"layers": {
"title": { "text": "Hello World" }
}
}'
Success Response:
{
"id": "render_abc123",
"status": "completed",
"template": "template_id_here",
"template_version": 3,
"created_at": "2026-02-01T14:22:00.000Z",
"completed_at": "2026-02-01T14:22:00.234Z",
"duration_ms": 234,
"output": {
"data": "data:image/png;base64,iVBORw0KGgo...",
"mime_type": "image/png",
"size_bytes": 45320,
"width": 1200,
"height": 630
},
"warnings": []
}
Response Fields:
| Field | Type | Description |
|---|---|---|
id | string | Unique render record ID |
status | string | Render status (completed) |
template | string | Template ID used for the render |
template_version | number | Template version at time of render |
created_at | string | ISO 8601 timestamp when the render was created |
completed_at | string | ISO 8601 timestamp when the render completed |
duration_ms | number | Render duration in milliseconds |
output.data | string | Base64-encoded data URI of the rendered output |
output.mime_type | string | MIME type (image/png, image/jpeg, or application/pdf) |
output.size_bytes | number | Size of the output in bytes |
output.width | number | Width of the rendered output |
output.height | number | Height of the rendered output |
output.page_count | number | Number of pages (PDF only, included when format is pdf) |
warnings | array | List of warning objects (see below) |
aws_s3 | object | S3 upload details (only present when useAws: true and upload succeeds) |
aws_s3.url | string | Public URL of the uploaded S3 object |
aws_s3.key | string | S3 object key |
aws_s3.bucket | string | S3 bucket name |
List Templates#
Lists all templates accessible by the authenticated API key.
GET /api/v1/templates
Example Request:
curl https://your-app.replit.app/api/v1/templates \
-H "Authorization: Bearer rs_abc123..."
Success Response:
{
"data": [
{
"id": "template_abc123",
"name": "Social Card",
"slug": "social-card",
"width": 1200,
"height": 630,
"version": 3,
"is_published": true,
"layer_schema": { ... },
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-02-01T14:22:00Z"
}
],
"total": 1
}
Get Template#
Retrieves a single template by ID.
GET /api/v1/templates/:id
Example Request:
curl https://your-app.replit.app/api/v1/templates/template_abc123 \
-H "Authorization: Bearer rs_abc123..."
Success Response:
{
"id": "template_abc123",
"name": "Social Card",
"slug": "social-card",
"width": 1200,
"height": 630,
"version": 3,
"is_published": true,
"layer_schema": { ... },
"canvas_data": { ... },
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-02-01T14:22:00Z"
}
Response Fields:
| Field | Type | Description |
|---|---|---|
id | string | Unique template ID |
name | string | Template display name |
slug | string | URL-friendly identifier used in API calls |
width | number | Canvas width in pixels |
height | number | Canvas height in pixels |
version | number | Current template version number |
is_published | boolean | Whether the template is published |
layer_schema | object | Schema describing dynamic layers |
canvas_data | object | Full canvas element data |
created_at | string | ISO 8601 creation timestamp |
updated_at | string | ISO 8601 last-updated timestamp |
Get Render#
Retrieves a single render record by ID.
GET /api/v1/renders/:id
Example Request:
curl https://your-app.replit.app/api/v1/renders/render_abc123 \
-H "Authorization: Bearer rs_abc123..."
Success Response:
{
"id": "render_abc123",
"userId": "user_id",
"templateId": "template_abc123",
"templateVersion": 3,
"status": "completed",
"format": "png",
"quality": 90,
"width": 1200,
"height": 630,
"durationMs": 234,
"fileSize": 45320,
"resultUrl": "https://...",
"warnings": [],
"layers": { ... },
"createdAt": "2026-02-01T14:22:00Z",
"completedAt": "2026-02-01T14:22:00.234Z"
}
Render Image (GET — Dynamic Image URL)#
Renders a template directly via URL query parameters. Returns the image as binary data suitable for use in <img> tags. Requires a GET-type API key (rs_get_...).
GET /api/v1/render?apiKey=YOUR_GET_KEY&template=TEMPLATE_SLUG
Authentication: API key is passed as a query parameter (apiKey), not in the Authorization header.
Requirements:
- A GET-type API key (prefix
rs_get_) - At least one active Allowed Host configured in Settings
- Request must originate from an allowed host
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | GET API key (starts with rs_get_) |
template | string | Yes | Template ID or slug |
width | number | No | Override output width in pixels |
height | number | No | Override output height in pixels |
format | string | No | png or jpeg only (default: png) |
quality | number | No | JPEG quality 1-100 (default: 90) |
{layer}.{prop} | string | No | Layer overrides in dot notation |
Layer Overrides via Dot Notation:
Use layerName.property=value as query parameters:
?title.text=Hello+World&title.fill=%23ff0000&logo.src=https://example.com/logo.png
Example Request:
<img src="https://your-app.replit.app/api/v1/render?apiKey=rs_get_abc123&template=social-card&title.text=Hello+World&subtitle.text=Dynamic+Images" />
Success Response:
- Status: 200
- Content-Type:
image/pngorimage/jpeg - Body: Raw binary image data
Response Headers:
| Header | Description |
|---|---|
X-Cache | HIT (cached) or MISS (freshly rendered) |
X-Render-Id | Unique render record ID (only on cache MISS) |
ETag | Cache key hash for browser caching |
Cache-Control | public, max-age=604800, immutable |
Rate Limits:
| Scope | Limit |
|---|---|
| Per API key | 60 requests/minute |
| Per IP address | 30 requests/minute |
| Cache hits | Count as 0.25x toward limits |
Limitations:
- Maximum URL length: 8,192 characters
- Maximum dimensions: 8,192 × 8,192 pixels
- No PDF format support (PNG and JPEG only)
See Dynamic Image URLs for complete documentation and examples.
Layer Overrides#
The layers object in render requests maps layer API names to property overrides. The key for each layer is the element's API Name from the editor — a snake_case identifier that is auto-generated from the layer's display Name (e.g., "Category Label" becomes category_label). You can view and customize the API Name in the Properties panel. The properties you can override depend on the layer type.
Auto-Hide Visibility: Layers with visibility mode set to Auto in the template editor are completely hidden from the rendered output when their content is empty or not provided — no placeholder, no border, nothing. Simply omit the layer from your
layersobject (or pass an empty value) to hide it. See Layers — Visibility Modes for details.
Text Layer Overrides#
{
"headline": {
"text": "Custom text content",
"fill": "#ff0000",
"fontSize": 48,
"fontFamily": "Arial"
}
}
| Property | Type | Description |
|---|---|---|
text | string | Text content to display |
fill | string | Text color (hex, rgb, or named color) |
fontSize | number | Font size in pixels |
fontFamily | string | Font family name |
fontWeight | string | Font weight: normal or bold |
fontStyle | string | Font style: normal or italic |
textAlign | string | Alignment: left, center, or right |
boxFill | string | Background fill color for the entire text element bounding box |
boxFillOpacity | number | Fill opacity 0–1 |
boxStroke | string | Border color for the text element box |
boxStrokeWidth | number | Border thickness value |
boxStrokeWidthUnit | string | Unit for border thickness: "px" (default) or "%" (percentage of min dimension) |
boxStrokeWidthMin | number | Minimum border thickness in px when using % unit |
boxStrokeWidthMax | number | Maximum border thickness in px when using % unit |
boxStrokeOpacity | number | Border opacity 0–1 |
boxStrokeAlign | string | "inner", "center" (default), or "outer" |
boxCornerRadius | number | Corner radius value for the text element box |
boxCornerRadiusUnit | string | Unit for corner radius: "px" (default) or "%" (percentage of min dimension) |
boxCornerRadiusMin | number | Minimum corner radius in px when using % unit |
boxCornerRadiusMax | number | Maximum corner radius in px when using % unit |
boxPaddingTop | number | Top padding between the box edge and text content |
boxPaddingRight | number | Right padding between the box edge and text content |
boxPaddingBottom | number | Bottom padding between the box edge and text content |
boxPaddingLeft | number | Left padding between the box edge and text content |
boxPaddingUnit | string | Unit for all padding values: "px" (default), "%-width" (% of element width), or "%-height" (% of element height) |
Image Layer Overrides#
{
"profile_photo": {
"src": "https://example.com/photo.jpg",
"fillMode": "cover"
}
}
| Property | Type | Description |
|---|---|---|
src | string | URL of the image to display |
fillMode | string | cover, contain, stretch, or original |
QR Code Layer Overrides#
{
"ticket_qr": {
"value": "https://example.com/ticket/12345",
"darkColor": "#1a1a2e",
"lightColor": "#ffffff",
"errorCorrectionLevel": "H",
"margin": 1
}
}
| Property | Type | Description |
|---|---|---|
value | string | Data to encode in the QR code (URL, text, vCard, etc.) |
darkColor | string | Color of QR modules (hex color, default: #000000) |
lightColor | string | Background color (hex color or transparent, default: #ffffff) |
errorCorrectionLevel | string | Error correction: L, M (default), Q, or H |
margin | number | Quiet zone in modules (0–10, default: 2) |
cornerRadius | number | Corner radius (px or % based on unit setting) |
cornerRadiusUnit | string | Unit for corner radius: "px" (default) or "%" |
Shape Layer Overrides#
{
"background_box": {
"fill": "#3b82f6",
"stroke": "#1d4ed8",
"strokeWidth": 2
}
}
| Property | Type | Description |
|---|---|---|
fill | string | Fill color |
stroke | string | Border/stroke color |
strokeWidth | number | Border thickness (px or % based on unit setting) |
strokeWidthUnit | string | Unit for stroke width: "px" (default) or "%" |
strokeWidthMin | number | Min stroke width in px when using % (optional) |
strokeWidthMax | number | Max stroke width in px when using % (optional) |
strokeAlign | string | Stroke alignment: "center" (default), "inner", or "outer" |
cornerRadius | number | Corner radius for rects (px or % based on unit setting) |
cornerRadiusUnit | string | Unit for corner radius: "px" (default) or "%" |
cornerRadiusMin | number | Min corner radius in px when using % (optional) |
cornerRadiusMax | number | Max corner radius in px when using % (optional) |
Transform Overrides (All Layer Types)#
Any layer can include transform overrides to reposition or resize elements at render time. When a layer uses percentage-based units for its transform properties (X, Y, Width, Height), each property has a configurable reference dimension that controls whether the percentage resolves against the canvas width or the canvas height.
{
"headline": {
"text": "Custom text",
"percentRefX": "width",
"percentRefY": "width",
"percentRefW": "width",
"percentRefH": "height"
}
}
| Property | Type | Default | Description |
|---|---|---|---|
percentRefX | "width" | "height" | "width" | Reference dimension for percentage-based X position. When set to "width", the X percentage resolves against the canvas width; when "height", it resolves against the canvas height. |
percentRefY | "width" | "height" | "width" | Reference dimension for percentage-based Y position. Defaults to width-based so that equal X and Y percentages produce equal pixel offsets. |
percentRefW | "width" | "height" | "width" | Reference dimension for percentage-based Width. |
percentRefH | "width" | "height" | "height" | Reference dimension for percentage-based Height. |
Pin Edge Overrides#
When a layer uses pinning (anchoring to canvas edges or other elements), each pin edge supports a percentRef field that controls the reference dimension for percentage-based offsets.
{
"headline": {
"text": "Pinned text",
"pinLeft": {
"enabled": true,
"target": "(edge)",
"offsetPercent": 10,
"unit": "%",
"percentRef": "width"
},
"pinTop": {
"enabled": true,
"target": "(edge)",
"offsetPercent": 10,
"unit": "%",
"percentRef": "width"
}
}
}
| Property | Type | Default | Description |
|---|---|---|---|
percentRef | "width" | "height" | "width" | Determines whether percentage-based pin offsets resolve against the canvas width or canvas height. When set to "width" (default), a 10% offset on any edge resolves to 10% of the canvas width, producing uniform spacing regardless of aspect ratio. Set to "height" when the offset should scale relative to the canvas height instead. |
Tip: Using width-based reference (the default) for all pin edges ensures that equal percentage values on different edges produce equal pixel distances. For example, setting 10% on both left and top pins with
percentRef: "width"means both offsets resolve against the canvas width, so the element maintains equal margins from the left and top edges even when the template is rendered at a different aspect ratio.
Warnings#
Warnings are returned when a render completes but encountered non-fatal issues. The render still produces an image, but some elements may not appear as expected.
Warning Object:
{
"element": "profile_photo",
"code": "IMAGE_LOAD_FAILED",
"message": "Could not load image: 404 Not Found"
}
Warning Codes:
| Code | Description |
|---|---|
IMAGE_LOAD_FAILED | Could not load an image from the provided URL |
IMAGE_URL_BLOCKED | Image URL was blocked for security (internal network, etc.) |
UNKNOWN_LAYER | A layer name in the request doesn't exist in the template |
PRESIGNED_URL_IN_LAYER | A layer image src is a short-lived pre-signed URL (GCS or AWS). The render succeeds but the URL will expire, making the render record and any future re-render unreliable. Replace with a stable, permanent URL. |
MISSING_DYNAMIC_LAYER | A dynamic template element was not provided in layers. The element's template default value was rendered in its place. |
S3_UPLOAD_FAILED | Failed to upload the rendered file to AWS S3 |
Rate Limits#
GET Render Endpoint#
The GET render endpoint enforces rate limits:
| Scope | Limit |
|---|---|
| Per API key | 60 requests/minute |
| Per IP address | 30 requests/minute |
| Cache hits | Count as 0.25x toward limits |
When rate-limited, the response includes a Retry-After header. The response status code is 429.
POST Render Endpoints#
POST render endpoints do not currently enforce strict rate limits, but excessive usage may be throttled. We recommend:
- Caching rendered images when the same data is used repeatedly
- Using reasonable image dimensions (avoid exceeding 4096x4096 unless needed)
- Batching requests where possible
Code Examples#
Node.js#
const response = await fetch('https://your-app.replit.app/api/v1/renders/sync', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
template: 'social-card',
layers: {
title: { text: 'Dynamic Title' },
subtitle: { text: 'Generated with RenderStack' },
},
}),
});
const imageBuffer = await response.arrayBuffer();
// Save or process the image buffer
Python#
import requests
response = requests.post(
'https://your-app.replit.app/api/v1/renders/sync',
headers={
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
json={
'template': 'social-card',
'layers': {
'title': {'text': 'Dynamic Title'},
'subtitle': {'text': 'Generated with RenderStack'},
},
},
)
with open('output.png', 'wb') as f:
f.write(response.content)
cURL#
curl -X POST https://your-app.replit.app/api/v1/renders/sync \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template":"social-card","layers":{"title":{"text":"Hello"}}}' \
-o output.png
Tip: You can auto-generate a
curlcommand pre-populated with your template's slug, dimensions, and dynamic layers directly from the template editor. Open your template and click the API Payload button (code icon) in the bottom-right corner of the canvas.