Base URL: https://api.devshot.dev · Machine-readable spec: /openapi.json
Two kinds of keys:
| Key | Use | How |
|---|---|---|
sk_live_… | server-side, POST requests | Authorization: Bearer sk_live_… |
pk_live_… | public, GET requests (meta tags) | ?key=pk_live_… |
Never put sk_ keys in client-side code or meta tags — that's what pk_ is for.
curl -X POST https://api.devshot.dev/v1/code \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"code": "const answer = 42;",
"language": "typescript",
"theme": "vitesse-dark",
"background": "gradient-purple",
"window": "mac",
"lineNumbers": false,
"highlightLines": [2, 5],
"scale": 2,
"format": "png"
}' > snippet.png
Returns the binary image. Headers: X-Cache: HIT|MISS, X-Render-Time-Ms.
Themes and backgrounds: GET /v1/themes.
Omit language (or pass "auto") and we'll detect it from the code.
Pro: pass "font" to change the code typeface —
jetbrains (default), fira-code, ibm-plex-mono, or source-code-pro.
Non-default fonts require the Pro plan (otherwise 403).
const res = await fetch('https://api.devshot.dev/v1/code', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.DEVSHOT_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ code, language: 'typescript', theme: 'nord' }),
});
await fs.promises.writeFile('snippet.png', Buffer.from(await res.arrayBuffer()));
curl -X POST https://api.devshot.dev/v1/og \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"template": "article",
"title": "How I built an API in 4 weekends",
"subtitle": "and what came out of it",
"author": "John Doe",
"avatar": "https://example.com/me.png",
"accent": "#3b82f6"
}' > og.png
Always 1200×630. Templates: article, minimal, code, stats.
template: "code" takes code (+ optional language) and renders a highlighted code window under the title — great for tutorial posts.
template: "stats" takes stats: "Stars:12.4k,Downloads:1M" (up to 4 pairs) for release/changelog cards.
No code at all: use the public key and query params. First request renders, every next one is a cache hit.
<meta property="og:image" content="https://api.devshot.dev/v1/og?title=My+Post&theme=dark&key=pk_live_..." />
export const metadata = {
openGraph: {
images: [`https://api.devshot.dev/v1/og?${new URLSearchParams({
title: post.title, author: post.author, key: process.env.NEXT_PUBLIC_DEVSHOT_PK,
})}`],
},
};
---
const og = `https://api.devshot.dev/v1/og?title=${encodeURIComponent(title)}&key=${import.meta.env.PUBLIC_DEVSHOT_PK}`;
---
<meta property="og:image" content={og} />
Same params as POST, but code is base64-encoded and highlightLines is comma-separated: ?code=Y29uc3Q...&highlightLines=2,5&key=pk_live_...
Your pk_ key is public by design — anyone who sees your meta tag can read it. Two protections:
1. Quota counts renders, not requests. Repeats of the same URL are cache hits and cost you nothing — replaying your URLs is pointless.
2. URL signing stops strangers from generating new images on your quota. Enable «Require signature» in the dashboard, then sign each URL with your signing secret — HMAC-SHA256 over the path+query, appended as the last parameter:
import crypto from 'node:crypto';
const url = `/v1/og?${new URLSearchParams({ title, key: process.env.DEVSHOT_PK })}`;
const sig = crypto.createHmac('sha256', process.env.DEVSHOT_SIGNING_SECRET)
.update(url).digest('hex');
const ogImage = `https://api.devshot.dev${url}&sig=${sig}`;
Unsigned mode stays available (default) — fine while your traffic is small; flip the toggle when it matters.
curl "https://api.devshot.dev/v1/me?key=pk_live_..." # or Authorization: Bearer sk_live_...
{ "email": "you@dev.io", "plan": "hobby", "month": "2026-07",
"used": 123, "limit": 5000, "remaining": 4877, "watermark": false }
Human version: devshot.dev/account. Every image response also carries an
X-Usage: used/limit header, so your client always knows the remaining quota.
| Status | Meaning |
|---|---|
401 | Missing/invalid key, or pk_ used for POST |
429 | Monthly plan limit reached (body contains an upgrade link) or rate limit (10 req/s per key). Only rendered images count — cache hits are free |
400 | Validation error — the body says which field |