ogmake

Quickstart

1. Get a key

Sign in at /login, create a key in the dashboard. Save it and its signing secret right away — the signing secret is shown once and is not stored anywhere you can retrieve it later.

Still on a hand-issued beta key? Email support@ogmake.com and we will send you an og_live_... API key and a signing secret the same way.

2. Render an image

POST /v1/images, Bearer-authenticated. Pass a template id and its params; the response is the stored image's URL, not the image bytes.

curl https://ogmake.com/v1/images \
  -H "Authorization: Bearer og_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "template": "blog",
    "params": {
      "site": "ogmake.com",
      "author": "Jane Doe",
      "date": "2026-09-19",
      "title": "Hello World"
    }
  }'
{
  "url": "https://ogmake.com/o/img/paid/k_.../<hash>.png",
  "hash": "<hash>",
  "cached": false,
  "width": 1200,
  "height": 630,
  "format": "png",
  "bytes": 48213,
  "templateVersion": "2"
}

A repeat request with the same template + params is a cache hit ("cached": true) and does not count against your quota.

3. Or link straight to a signed image URL

GET /i/{keyId}/{sig}?...params renders on first request and serves from cache after that — put it straight in <meta property="og:image">, no server round trip needed. sig is an HMAC-SHA256 over the request's own canonical query string, using your key's signing secret.

The canonical string: drop sig and debug, sort remaining keys by codepoint, RFC3986-encode each key=value, join with &. Node example (save as sign.mjs, run with node sign.mjs) — this builds the canonical string from params rather than hard-coding it, and this exact input reproduces the fixed test vector for signing. Python, Ruby, PHP and Go versions of the same script are on the signing page.

The core of it — building the canonical string, then the HMAC over it. The whole runnable file, imports and test vector included, is the next line down.

// Keys excluded from the canonical query (must match packages/core/src/signing.ts).
const CANONICAL_EXCLUDED_KEYS = new Set(["sig", "debug"]);

// Drops `sig`/`debug`, sorts params by key codepoint, RFC3986-encodes each
// "key=value", joins with "&".
function canonicalQuery(params) {
  return Object.keys(params)
    .filter((key) => !CANONICAL_EXCLUDED_KEYS.has(key))
    .sort()
    .map((key) => `${rfc3986Encode(key)}=${rfc3986Encode(params[key])}`)
    .join("&");
}

async function signQuery(secretBase64, canonical) {
  const secret = Buffer.from(secretBase64, "base64");
  const key = await subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, [
    "sign",
  ]);
  const mac = await subtle.sign("HMAC", key, new TextEncoder().encode(canonical));
  return base64UrlEncode(new Uint8Array(mac));
}
Whole file — sign.mjs
// docs/snippets/sign.mjs — WP5 step 42 (docs/WP3_WP6_BREAKDOWN.md).
// Builds the canonical query string for a signed `GET /i/{keyId}/{sig}` URL
// and signs it (HMAC-SHA256, base64url). Prints the canonical string, then
// the signature, one per line.
//
// This is the fixed test vector shared with packages/core/src/signing.test.ts
// and tooling/src/snippets.test.ts — running this script must print exactly:
//   format=png&template=basic&title=Hello%20World
//   W8Xvrf9UTFKbVVQtofHq7A2ljK9z-nvJUL3B-rB7MY8
import { webcrypto } from "node:crypto";

const { subtle } = webcrypto;

// Your key's signing secret, base64 (shown once when the key is issued).
const SIGNING_SECRET_BASE64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
const PARAMS = { template: "basic", title: "Hello World", format: "png" };

function base64UrlEncode(bytes) {
  return Buffer.from(bytes)
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

// RFC3986 encoding: encodeURIComponent leaves !'()* unescaped; escape those too.
function rfc3986Encode(value) {
  return encodeURIComponent(value).replace(
    /[!'()*]/g,
    (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
  );
}

// Keys excluded from the canonical query (must match packages/core/src/signing.ts).
const CANONICAL_EXCLUDED_KEYS = new Set(["sig", "debug"]);

// Drops `sig`/`debug`, sorts params by key codepoint, RFC3986-encodes each
// "key=value", joins with "&".
function canonicalQuery(params) {
  return Object.keys(params)
    .filter((key) => !CANONICAL_EXCLUDED_KEYS.has(key))
    .sort()
    .map((key) => `${rfc3986Encode(key)}=${rfc3986Encode(params[key])}`)
    .join("&");
}

async function signQuery(secretBase64, canonical) {
  const secret = Buffer.from(secretBase64, "base64");
  const key = await subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, [
    "sign",
  ]);
  const mac = await subtle.sign("HMAC", key, new TextEncoder().encode(canonical));
  return base64UrlEncode(new Uint8Array(mac));
}

const canonical = canonicalQuery(PARAMS);
console.log(canonical);
console.log(await signQuery(SIGNING_SECRET_BASE64, canonical));

The resulting URL:

https://ogmake.com/i/k_.../W8Xvrf9UTFKbVVQtofHq7A2ljK9z-nvJUL3B-rB7MY8?format=png&template=basic&title=Hello%20World

?debug=1 appended to a signed URL still verifies (it is excluded from the canonical string) and returns JSON instead of image bytes, for troubleshooting.

Rate limits and quota errors

Response Meaning
401 { "error": "unauthorized" } Missing or unknown key.
401 { "error": "key_revoked" } Key exists but has been revoked.
429 { "error": "rate_limited" } Per-key burst limit. Retry-After: 10 (seconds).
429 { "error": "quota_exceeded", cap, credits, resetsAt } Past your monthly cap and its grace. resetsAt is the next billing month's start, unix seconds.

Signed GET /i never returns an error status for a valid signature — an over-cap request instead gets a watermarked card (within a limited grace budget), then falls back to whatever's already cached for those exact params, clean or watermarked; a genuinely new param combination past that budget gets a generic fallback image instead. An og:image URL never breaks. See the pricing page for cap sizes and the grace window.

Where to next

Something not behaving the way this page says? Email support@ogmake.com.