ogdynamic

The ogdynamic Render API reference.

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.

Quickstart

You do not need an API key to render images. The API key is only required to manage canvases and stored renders programmatically.

01

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.

02

Take the image URL

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.

03

Override dynamic layers

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

Authentication

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:readGET /me
canvases:readGET /canvases, GET /canvases/{canvas}
canvases:renderPOST /renders, GET /renders, DELETE /renders/{render}
webhooks:manageGET /webhooks, POST /webhooks/subscribe, DELETE /webhooks/{webhook}

Canvases

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.

GET /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"
    }
  ]
}
GET /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"
  }
}

Image URLs

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.
formatOutput 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.
_updatedAtCache-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.
POST 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.

Stored renders

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.

POST /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
contenttext layersstring, max 2000 chars — replaces the layer’s text
srcimage layershttp(s) URL, max 2048 chars — replaces the image source
colortext layershex color (#rgb, #rgba, #rrggbb, #rrggbbaa) — replaces the text color
backgroundColorany layerhex 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"
  }
}
  • The render's id is a SHA-256 hash of the canvas id + canonicalized override — the same payload is stored once. Re-submitting returns the existing render ( 200 OK instead of 201 Created).
  • The returned 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.
  • Editing the canvas updates its render URLs automatically (within the image service's 10 second cache window) — stored renders track the live canvas, not a frozen copy.
  • Works for private canvases: creating a render is the owner's consent to publish that exact composition. Deleting the canvas deletes all of its renders.
GET /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"
    }
  ]
}
DELETE /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

Webhooks

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.

canvas.createdcanvas.updatedrender.created
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.

Errors

Status Meaning
401Unauthenticated — missing or invalid token.
403Forbidden — the resource belongs to another user, or the canvas is private.
404Not found — the resource does not exist.
422Validation failed — the response body lists the offending fields.

Canvas document format

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

Node fields

Field Description
id stringStable, readable id — never reused inside a document.
type stringOne of text, image, or group (groups hold children — a real tree, any depth).
name stringHuman 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 objectcamelCase CSS properties (see below). Position lives here as style.left / style.top — there is no first-class x/y.
text stringText nodes only — the layer’s content.
src stringImage nodes only — any remote URL the renderer can fetch.
children arrayGroup nodes only — nested CanvasNodes.
dynamic booleanMarks the layer’s content as overridable at render time.
fieldId stringThe override key: lowercase [a-z0-9_], no leading underscore. ?{fieldId}=… swaps the layer’s content.
background FillThe layer’s background fill — same Fill shape as the canvas root.

Positioning & styles

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.

Next step

Design once on the canvas — render everything else from a URL.

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.