# Agent & LLM Integration — FastOG A complete, self-contained guide for integrating FastOG from any server-side language — written for both human developers and AI coding agents. The whole client surface is one function: `signedUrl(params, key, secret)` → a ready-to-embed `` URL. ## Download the skill Take the whole skill with you — drop both files into your `agents/skills/fastog-integration/` directory to load it in Claude Code, Codex, or any agent: - [`SKILL.md`](https://fastog.com/skill/SKILL.md) — `curl -O https://fastog.com/skill/SKILL.md` - [`implementations.md`](https://fastog.com/skill/implementations.md) — `curl -O https://fastog.com/skill/implementations.md` Prefer one combined file? This page **is** the skill (contract + implementations inline): `curl -O https://fastog.com/docs-files/agent-integration.md` ## The contract (the only one) ``` URL = https://fastog.com/api/v1/og?&key=&s= canonical = every query param except `s` (INCLUDING `key`), keys sorted, array values sorted, each pair enc(key)=enc(value) RFC-3986, joined with "&" message = "GET/api/v1/og" + "\n" + canonical signature = hex(HMAC-SHA256(message, hmac_secret)) ``` ### Invariants (breaking any one → 401) 1. **RFC-3986 encoding** — percent-encode everything except `A-Za-z0-9-._~`; **space → `%20`, never `+`**. PHP `rawurlencode`; JS `encodeURIComponent` + re-encode `!'()*`; Python `quote(safe='-._~')`; Ruby/Go custom byte encoder. 2. **`key` is signed** — it is part of the canonical query, so the signature is bound to the key. 3. **Arrays need bracket keys in the URL** — sign the canonical with the plain key (`features=…&features=…`), but the URL must use `features[0]=…` or `features[]=…`. The backend is PHP: repeated non-bracket keys (`features=a&features=b`) decode to a **scalar** (last wins) → canonical mismatch → 401. 4. **No timestamp** — content-addressed. Identical params → identical URL → cacheable forever; any change → 401 → no render, no quota consumed. Reserved params: `s` (signature — excluded from canonical), `key` (included), `t` (reserved — never use). ### Security (non-negotiable) - `hmac_secret` is **private** — server-side only. Never in a browser, never in a public CDN script. Client-side/browser signing can never work. - `key` is **public** (it appears in every URL) — expected and safe, because forging needs the secret. - Only **GET** (crawlers never POST). Sign on the server at request time; the signed URL is what you embed. ### Worked example ``` params = {template:"blog", title:"Hello World", features:["Free shipping","30-day returns"], key:"sk_test123"} secret = 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef canonical = features=30-day%20returns&features=Free%20shipping&key=sk_test123&template=blog&title=Hello%20World signature = a79232b15b45002aee76174f542e0a661b37cb1259df01e263c952ca4052a54d url = https://fastog.com/api/v1/og?template=blog&title=Hello%20World&features%5B0%5D=Free%20shipping&features%5B1%5D=30-day%20returns&key=sk_test123&s=a79232b15b45002aee76174f542e0a661b37cb1259df01e263c952ca4052a54d ``` ## One-file signers (zero deps, no package manager) There's no `npm i`/`pip install` package yet — these are **single-file signers**: each downloads as one file and only produces the signed OG image URL (all HMAC + RFC-3986 logic, no runtime). Download and sign on the client's server: | Language/Framework | File | One-liner | | ----------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------- | | Node.js (CJS) / Express | `https://fastog.com/sdk/node.js` · `/sdk/express/fastog.js` | `ogImageUrl(params, { key, secret })` | | ESM (SvelteKit / Next / Astro / Vue-Nuxt) | `/sdk/svelte/fastog.js` · `/sdk/next/fastog.js` · `/sdk/astro/fastog.js` · `/sdk/vue/fastog.js` | `ogImageUrl(params, { key, secret })` | | PHP | `https://fastog.com/sdk/php/fastog.php.txt` | `FastOg::signedUrl($params, $key, $secret)` | | Ruby / Rails | `https://fastog.com/sdk/ruby/fastog.rb` | `FastOg.signed_url(params, key, secret)` | | Python | `https://fastog.com/sdk/python/fastog.py` | `fastog.signed_url(params, key, secret)` | | Go | `https://fastog.com/sdk/go/fastog.go` | `fastog.SignedURL(params, key, secret)` | Embed the result: ```html ``` ## Golden vector (self-verify ANY implementation) ``` params = {template:"blog", title:"Hello World", features:["Free shipping","30-day returns"], key:"sk_test123"} secret = 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef canonical = features=30-day%20returns&features=Free%20shipping&key=sk_test123&template=blog&title=Hello%20World signature = a79232b15b45002aee76174f542e0a661b37cb1259df01e263c952ca4052a54d ``` An implementation is correct **iff** it produces that canonical and that signature for those inputs — and a different param/key must change the signature. The golden vector is generated by the FastOG server itself, so it is the ground truth. ## Reference implementations (no SDK / any language) ### JavaScript (Node.js, CommonJS) ```js const crypto = require("crypto"); const enc = (s) => encodeURIComponent(String(s)).replace( /[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(), ); function canonicalize(params, apiKey) { const all = { ...params, key: apiKey }; const pairs = []; for (const [k, v] of Object.entries(all)) { for (const vv of Array.isArray(v) ? [...v].sort() : [v]) { pairs.push(`${enc(k)}=${enc(vv)}`); } } return pairs.sort().join("&"); } function sign(params, apiKey, secret) { return crypto .createHmac("sha256", secret) .update(`GET/api/v1/og\n${canonicalize(params, apiKey)}`) .digest("hex"); } function signedUrl( params, apiKey, secret, baseUrl = "https://fastog.com/api/v1/og", ) { const url = new URL(baseUrl); for (const [k, v] of Object.entries({ ...params, key: apiKey })) { const isArray = Array.isArray(v); for (const vv of isArray ? v : [v]) { url.searchParams.append(isArray ? `${k}[]` : k, vv); } } url.searchParams.set("s", sign(params, apiKey, secret)); return url.toString(); } module.exports = { signedUrl, sign, canonicalize }; ``` ### PHP ```php function fastog_canonicalize(array $params, string $apiKey): string { $pairs = []; foreach (array_merge($params, ['key' => $apiKey]) as $k => $v) { $values = is_array($v) ? $v : [$v]; sort($values, SORT_STRING); foreach ($values as $vv) { $pairs[] = rawurlencode((string) $k) . '=' . rawurlencode((string) $vv); } } sort($pairs, SORT_STRING); return implode('&', $pairs); } function fastog_sign(array $params, string $apiKey, string $secret): string { return hash_hmac('sha256', 'GET/api/v1/og' . "\n" . fastog_canonicalize($params, $apiKey), $secret); } function fastog_signed_url(array $params, string $apiKey, string $secret, string $baseUrl = 'https://fastog.com/api/v1/og'): string { $all = array_merge($params, ['key' => $apiKey, 's' => fastog_sign($params, $apiKey, $secret)]); return $baseUrl . '?' . http_build_query($all, '', '&', PHP_QUERY_RFC3986); } ``` ### Python ```python import hashlib, hmac from urllib.parse import quote _enc = lambda s: quote(str(s), safe="-._~") def canonicalize(params, api_key): pairs = [] for k, v in {**params, "key": api_key}.items(): for vv in sorted(v) if isinstance(v, list) else [v]: pairs.append(f"{_enc(k)}={_enc(vv)}") return "&".join(sorted(pairs)) def sign(params, api_key, secret): return hmac.new(secret.encode(), f"GET/api/v1/og\n{canonicalize(params, api_key)}".encode(), hashlib.sha256).hexdigest() def signed_url(params, api_key, secret, base_url="https://fastog.com/api/v1/og"): all_p = {**params, "key": api_key, "s": sign(params, api_key, secret)} pairs = [] for k, v in all_p.items(): if isinstance(v, list): for i, vv in enumerate(v): pairs.append(f"{_enc(f'{k}[{i}]')}={_enc(vv)}") else: pairs.append(f"{_enc(k)}={_enc(v)}") return f"{base_url}?{'&'.join(pairs)}" ``` ### Ruby ```ruby require "openssl" def enc(s) s.to_s.gsub(/[^A-Za-z0-9\-._~]/) { |c| c.bytes.map { |b| "%%%02X" % b }.join } end def canonicalize(params, api_key) pairs = params.merge("key" => api_key).flat_map do |k, v| values = v.is_a?(Array) ? v.sort : [v] values.map { |vv| "#{enc(k)}=#{enc(vv)}" } end pairs.sort.join("&") end def sign(params, api_key, secret) OpenSSL::HMAC.hexdigest("SHA256", secret, "GET/api/v1/og\n#{canonicalize(params, api_key)}") end def signed_url(params, api_key, secret, base_url = "https://fastog.com/api/v1/og") all = params.merge("key" => api_key, "s" => sign(params, api_key, secret)) pairs = all.flat_map do |k, v| if v.is_a?(Array) v.each_with_index.map { |vv, i| "#{enc("#{k}[#{i}]")}=#{enc(vv)}" } else ["#{enc(k)}=#{enc(v)}"] end end "#{base_url}?#{pairs.join("&")}" end ``` ### Go ```go import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "sort" "strings" ) func enc(s string) string { const hex = "0123456789ABCDEF" var b strings.Builder for i := 0; i < len(s); i++ { c := s[i] if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '~' { b.WriteByte(c) } else { b.WriteByte('%') b.WriteByte(hex[c>>4]) b.WriteByte(hex[c&0x0f]) } } return b.String() } func canonicalize(params map[string]any, apiKey string) string { all := map[string]any{} for k, v := range params { all[k] = v } all["key"] = apiKey keys := make([]string, 0, len(all)) for k := range all { keys = append(keys, k) } sort.Strings(keys) var pairs []string for _, k := range keys { vals := []string{fmt.Sprint(all[k])} if arr, ok := all[k].([]string); ok { vals = arr sort.Strings(vals) } for _, v := range vals { pairs = append(pairs, enc(k)+"="+enc(v)) } } sort.Strings(pairs) return strings.Join(pairs, "&") } func sign(params map[string]any, apiKey, secret string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte("GET/api/v1/og\n" + canonicalize(params, apiKey))) return hex.EncodeToString(mac.Sum(nil)) } func signedURL(params map[string]any, apiKey, secret, baseURL string) string { all := map[string]any{} for k, v := range params { all[k] = v } all["key"] = apiKey all["s"] = sign(params, apiKey, secret) keys := make([]string, 0, len(all)) for k := range all { keys = append(keys, k) } sort.Strings(keys) var pairs []string for _, k := range keys { if arr, ok := all[k].([]string); ok { for i, v := range arr { pairs = append(pairs, enc(k+"["+fmt.Sprint(i)+"]")+"="+enc(v)) } } else { pairs = append(pairs, enc(k)+"="+enc(fmt.Sprint(all[k]))) } } return baseURL + "?" + strings.Join(pairs, "&") } ``` ## Debugging 401 | Code | Meaning | Fix | | ------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `missing_signature` | URL missing `key` or `s` | regenerate with a signer; verify both params are present | | `invalid_signature` | canonical mismatch or wrong secret | re-sign after ANY content change; confirm the same `hmac_secret` on both sides; check space is `%20` (not `+`); check arrays use bracket keys in the URL; confirm `key` is in the canonical |