URL rendering and OG images

Signed image URLs for CMS embeds and Open Graph tags, without sending an API key on fetch.

<img> and og:image cannot send Authorization. Mint a signed URL with your API key, then let crawlers fetch it anonymously.

Mint

POST /api/v1/img/sign
Authorization: Bearer sk_...
Content-Type: application/json

{
  "template": "<design-id>",
  "format": "png",
  "layers": { "headline": { "text": "Hello" } }
}
{
  "url": "https://app.jsontoimg.com/api/v1/img/<design-id>?headline=Hello&version=1&sig=…",
  "expiresAt": null
}

Put url in <img src> or <meta property="og:image">. Optional expiresIn (seconds) sets exp on the URL.

TypeScript:

const { url } = await client.signImageUrl({
  template: "<design-id>",
  format: "png",
  layers: { headline: { text: post.title } },
});

Minting does not consume a credit. The first unique GET of each URL does.

Batch mint

Pass jobs (1–50) on the same endpoint. This mints CMS / OG links; it does not queue renders (POST /api/v1/renders/batch still does that).

POST /api/v1/img/sign
Authorization: Bearer sk_...
Content-Type: application/json

{
  "jobs": [
    { "template": "<design-id>", "layers": { "headline": { "text": "Hello" } } },
    { "template": "<design-id>", "layers": { "headline": { "text": "World" } } }
  ]
}
{
  "urls": [
    { "url": "https://app.jsontoimg.com/api/v1/img/<design-id>?headline=Hello&version=1&sig=…", "expiresAt": null },
    { "url": "https://app.jsontoimg.com/api/v1/img/<design-id>?headline=World&version=1&sig=…", "expiresAt": null }
  ]
}
const { urls } = await client.signImageUrls({
  jobs: [
    { template: "<design-id>", layers: { headline: { text: "Hello" } } },
    { template: "<design-id>", layers: { headline: { text: "World" } } },
  ],
});

Fetch

GET that URL with no API key. The first unique payload renders (one credit, billed to the template owner). The same title again streams the stored file.

Tampering with query params (for example changing the headline) invalidates sig → 401, no render. Duplicate query keys (for example headline=Hello&…&headline=Injected) are rejected with 400 before verification.

If the first render is still running, the response is 503 with Retry-After (not JSON). Crawlers retry.

Completed jobs

GET /api/v1/renders/{id} (and wait / webhooks / batch) returns assetUrl with ?sig=. That URL also works in <img> until the file is archived (410).

Authenticated GET /asset without sig still streams bytes for API clients.

Dry-run

POST /api/v1/render with "dryRun": true (or ?dryRun=1) validates layers and variables and returns { valid: true, … } with no job and no credit.

JPEG

format: "jpeg" is accepted and stored as jpg.

On this page