Design a canvas
Create a canvas in the editor — start from a template or a blank canvas. Mark the layers you want to swap at render time as dynamic; each one gets a field id (e.g. title) you will use as a query parameter.
Design a canvas once, then render every variation from a URL — or store permanent, deduplicated renders over REST. This page covers authentication, canvas endpoints, image URL semantics, stored renders, webhooks, and the canvas document format behind every render.
You do not need an API key to render images. The API key is only required to manage canvases and stored renders programmatically.
Create a canvas in the editor — start from a template or a blank canvas. Mark the layers you want to swap at render time as dynamic; each one gets a field id (e.g. title) you will use as a query parameter.
Every public canvas has a permanent image URL: https://cdn.ogdynamic.com/c/{canvasId}. Embed it anywhere — meta tags, emails, markdown. The image re-renders automatically whenever you edit the canvas.
Append ?field=value parameters to swap dynamic content per request — no API call required for one-off renders. When you need a stable, shareable URL for an exact composition, store it as a render via the API.
# The canvas image URL — dynamic layers swap per request https://cdn.ogdynamic.com/c/01m3ef2xmqeapjrqnw6fhs5ye6?title=Prestige+GMT+Automatic&price=%242400 # Fetch it as PNG curl "https://cdn.ogdynamic.com/c/01m3ef2xmqeapjrqnw6fhs5ye6?title=Prestige+GMT&format=png" -o og.png
All /api/v1 endpoints authenticate with an OAuth bearer token. Send it in the Authorization header and request only the scopes your client needs:
curl https://ogdynamic.com/api/v1/me \
-H "Authorization: Bearer {token}"| Scope | Grants |
|---|---|
| user:read | GET /me |
| canvases:read | GET /canvases, GET /canvases/{canvas} |
| canvases:render | POST /renders, GET /renders, DELETE /renders/{render} |
| webhooks:manage | GET /webhooks, POST /webhooks/subscribe, DELETE /webhooks/{webhook} |
A canvas is a designed document — dimensions, a background fill, and a layer tree. Canvases are created and edited in the visual editor; the API reads them and exposes each dynamic layer as an overridable field.
/api/v1/canvases canvases:read Lists your canvases with their public image URLs. Optional exact-match filters: status=public or privatewidth=exact width in px (1–1920)height=exact height in px (1–1920)
curl https://ogdynamic.com/api/v1/canvases \
-H "Authorization: Bearer {token}"
# Optional exact-match filters
curl "https://ogdynamic.com/api/v1/canvases?status=public&width=1200" \
-H "Authorization: Bearer {token}"{
"data": [
{
"id": "01m3ef2xmqeapjrqnw6fhs5ye6",
"user_id": 1,
"name": "Product card",
"width": 1200,
"height": 630,
"status": "public",
"image_url": "https://cdn.ogdynamic.com/c/01m3ef2xmqeapjrqnw6fhs5ye6",
"created_at": "2026-10-01T09:12:00.000000Z",
"updated_at": "2026-10-20T17:40:00.000000Z"
}
]
}/api/v1/canvases/{canvas} canvases:read Returns one canvas with its full document and an override_fields list — every dynamic layer, its field id, and the default that renders when the parameter is absent.
{
"data": {
"id": "01m3ef2xmqeapjrqnw6fhs5ye6",
"user_id": 1,
"name": "Product card",
"width": 1200,
"height": 630,
"status": "public",
"background": { "kind": "solid", "color": "#0f172a" },
"items": [ "… the full layer tree …" ],
"image_url": "https://cdn.ogdynamic.com/c/01m3ef2xmqeapjrqnw6fhs5ye6",
"override_fields": [
{ "field": "title", "name": "Title", "type": "text", "default": "Prestige GMT Automatic" },
{ "field": "price", "name": "Price", "type": "text", "default": "$2400" },
{ "field": "cover", "name": "Cover image", "type": "image", "default": "https://example.com/watch.jpg" }
],
"created_at": "2026-10-01T09:12:00.000000Z",
"updated_at": "2026-10-20T17:40:00.000000Z"
}
}Every public canvas renders on a permanent URL, served by the ogdynamic image service — the same engine the editor's live preview uses, so preview and output are pixel-identical. No authentication: anyone with the URL can fetch the image.
https://cdn.ogdynamic.com/c/{canvasId}?format=webp&title=Hello&_updatedAt=1750000000| Parameter | Effect |
|---|---|
| ?{fieldId}=… | Dynamic-layer override: replaces a text layer’s text or an image layer’s src. Unknown keys are ignored. |
| format | Output format: webp (default), png, or jpeg. |
| config[format] | Same as format, but wins over the flat param. |
| config[intent] | Re-render at an intent’s size: og → 1200×630, twitter → 1200×600. Percentage-based layouts reflow automatically. |
| _updatedAt | Cache-buster stamp. With it the response is cached for 30 days as immutable — change the stamp when the canvas changes. Use the canvas’s updated_at epoch. |
https://cdn.ogdynamic.com/c/{canvasId} Prefer a JSON body over a long query string? POST the same overrides as a flat {fieldId: value} object. Body entries win over same-named query params; values must be scalars.
curl -X POST https://cdn.ogdynamic.com/c/01m3ef2xmqeapjrqnw6fhs5ye6 \
-H "Content-Type: application/json" \
-d '{"title": "Prestige GMT Automatic", "price": "$2400"}' Private canvases have no public image URL — their documents leave the app only through stored renders (below). Renders are deterministic: an identical document and parameters always produce byte-identical images, which is what makes the immutable _updatedAt caching safe.
Query parameters render one-off images on the fly. When you want a stable, shareable URL for an exact composition — personalized campaign images, saved social posts, records you list later — store it as a render.
/api/v1/renders canvases:render Stores a render of one canvas with your overrides baked in. Each override key must match a dynamic layer's field id from override_fields:
| Property | Applies to | Value |
|---|---|---|
| content | text layers | string, max 2000 chars — replaces the layer’s text |
| src | image layers | http(s) URL, max 2048 chars — replaces the image source |
| color | text layers | hex color (#rgb, #rgba, #rrggbb, #rrggbbaa) — replaces the text color |
| backgroundColor | any layer | hex color, or transparent — replaces the layer’s background fill |
curl -X POST https://ogdynamic.com/api/v1/renders \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"canvasId": "01m3ef2xmqeapjrqnw6fhs5ye6",
"override": {
"title": { "content": "Prestige GMT Automatic", "color": "#635bff" },
"price": { "content": "$2400" },
"cover": { "src": "https://cdn.shop.example/watch.jpg" }
}
}'{
"data": {
"id": "9f2c4a1b0d7e4f2a8c3b5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a",
"canvas_id": "01m3ef2xmqeapjrqnw6fhs5ye6",
"image_url": "https://cdn.ogdynamic.com/c/01m3ef2xmqeapjrqnw6fhs5ye6?_key=9f2c4a1b…",
"payload": {
"title": { "content": "Prestige GMT Automatic", "color": "#635bff" },
"price": { "content": "$2400" },
"cover": { "src": "https://cdn.shop.example/watch.jpg" }
},
"created_at": "2026-10-21T10:02:00.000000Z",
"updated_at": "2026-10-21T10:02:00.000000Z"
}
}200 OK instead of 201 Created). image_url is permanent and safe to embed as-is. The composition is immutable: query-param or POST-body overrides have no effect on it. Format options like ?format=png and ?config[intent]=twitter still work. /api/v1/renders?canvasId={canvasId} canvases:render Lists your renders, newest first. Append ?canvasId= to scope the list to a single canvas — filtering by a canvas you do not own simply returns an empty list. List responses include the source canvas's {id, name}.
# All of your renders, newest first
curl https://ogdynamic.com/api/v1/renders \
-H "Authorization: Bearer {token}"
# Renders of one canvas
curl "https://ogdynamic.com/api/v1/renders?canvasId=01m3ef2xmqeapjrqnw6fhs5ye6" \
-H "Authorization: Bearer {token}"{
"data": [
{
"id": "9f2c4a1b0d7e4f2a8c3b5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a",
"canvas_id": "01m3ef2xmqeapjrqnw6fhs5ye6",
"image_url": "https://cdn.ogdynamic.com/c/01m3ef2xmqeapjrqnw6fhs5ye6?_key=9f2c4a1b…",
"payload": { "title": { "content": "Prestige GMT Automatic", "color": "#635bff" } },
"canvas": { "id": "01m3ef2xmqeapjrqnw6fhs5ye6", "name": "Product card" },
"created_at": "2026-10-21T10:02:00.000000Z",
"updated_at": "2026-10-21T10:02:00.000000Z"
}
]
}/api/v1/renders/{render} canvases:render Deletes a render by id. The image URL stops resolving once the image service's 10 second document cache expires; images already in its cache may be served until evicted.
curl -X DELETE https://ogdynamic.com/api/v1/renders/9f2c4a1b… \
-H "Authorization: Bearer {token}"
# → 204 No Content Subscribe a https URL to be notified the moment a canvas or render changes. Every delivery is signed with X-Webhook-Signature: sha256={hmac_sha256(raw_body, secret)} — the signing secret is returned exactly once, when you subscribe. Failing targets are retried with backoff and auto-revoked after repeated failures.
curl -X POST https://ogdynamic.com/api/v1/webhooks/subscribe \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"event": "render.created",
"target_url": "https://hooks.example.com/ogdynamic"
}' Also available: GET /api/v1/webhooks lists your subscriptions (the secret is never included), and DELETE /api/v1/webhooks/{webhook} unsubscribes idempotently. All three require the webhooks:manage scope — this is the transport behind the Zapier integration.
| Status | Meaning |
|---|---|
| 401 | Unauthenticated — missing or invalid token. |
| 403 | Forbidden — the resource belongs to another user, or the canvas is private. |
| 404 | Not found — the resource does not exist. |
| 422 | Validation failed — the response body lists the offending fields. |
Every canvas is a single JSON object — the design system the editor persists, templates ship, and the render service consumes. Width and height are the viewport in pixels (1–1920, up to 1080p resolution); background and every node carry a Fill that keeps all three branches populated, with kind selecting the active one:
{
"width": 1200,
"height": 630,
"background": {
"kind": "gradient",
"color": "#0f172a",
"gradient": {
"type": "linear",
"angle": 135,
"stops": [
{ "color": "#635bff", "position": 0 },
{ "color": "#0f172a", "position": 100 }
]
},
"image": { "source": null }
},
"items": [
{
"id": "tpl-title",
"type": "text",
"name": "Title",
"width": 940,
"height": 240,
"style": { "position": "absolute", "left": 80, "top": 196 },
"text": "Prestige GMT Automatic",
"dynamic": true,
"fieldId": "title",
"children": [],
"background": { "kind": "solid", "color": "transparent", "gradient": null, "image": { "source": null } }
}
]
}| Field | Description |
|---|---|
| id string | Stable, readable id — never reused inside a document. |
| type string | One of text, image, or group (groups hold children — a real tree, any depth). |
| name string | Human label shown in the editor’s Layers panel. |
| width / height number | "100%" | Px numbers, or "100%" for full-bleed bands. Mandatory for image nodes; text/group nodes may omit them to auto-size. |
| style object | camelCase CSS properties (see below). Position lives here as style.left / style.top — there is no first-class x/y. |
| text string | Text nodes only — the layer’s content. |
| src string | Image nodes only — any remote URL the renderer can fetch. |
| children array | Group nodes only — nested CanvasNodes. |
| dynamic boolean | Marks the layer’s content as overridable at render time. |
| fieldId string | The override key: lowercase [a-z0-9_], no leading underscore. ?{fieldId}=… swaps the layer’s content. |
| background Fill | The layer’s background fill — same Fill shape as the canvas root. |
There is no first-class x/y — position is style.left / style.top. Absolute layers anchor to the viewport; static layers participate in flex flow (full-bleed bands use "width": "100%" inside a flex-column group). Styles are camelCase CSS property names passed verbatim to the rendering engine — bare numbers mean px:
01 Layout — position, top/right/bottom/left, display, zIndex, boxSizing
02 Sizing — width/height (+ min/max), aspectRatio
03 Spacing — margin*, padding* (shorthand + longhands)
04 Flexbox — flexDirection, flexWrap, flexGrow/Shrink/Basis, justifyContent, alignItems, gap, order
05 Border & radius — border{Side}Width, borderStyle, borderColor, borderRadius (per-corner too)
06 Background — backgroundColor, backgroundImage, backgroundSize/Position/Repeat
07 Effects — opacity, overflow, boxShadow, filter, backdropFilter, transform, rotate, clipPath, mixBlendMode
08 Typography — color, fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, textAlign, textShadow, maxLines, lineClamp, …
09 Image — objectFit, objectPosition, imageRendering
fontFamily must be one of the render service's registered families — 67 typefaces including Inter, Geist, Space Grotesk, Playfair Display, Bebas Neue, and JetBrains Mono. Dynamic layers apply recursively at any depth: mark a node dynamic: true with a fieldId, and ?{fieldId}=… on the image URL swaps its content at render time. Param ids are lowercase [a-z0-9_] without a leading underscore — the _ prefix is reserved for app parameters like _updatedAt.
Start from a template, mark your dynamic layers, and the Render API takes over from there. Need a hand wiring it into your stack? Reach out.