HypeCard is a social share-card platform. You define HTML templates
with Handlebars {{variable}} tokens, create "shares" (instances with
filled-in data), and get back a public OG-image URL suitable for social
previews and deep-link redirects.
Each template version has a format field:
| Format | Dimensions | Use Case |
|---|---|---|
og | 1200×630 | Open Graph images (default) |
story | 1080×1920 | Instagram Stories, TikTok, Snapchat |
A single template container can hold versions of any format.
When creating a share, specify which format you want — the platform
picks the latest version matching that format.
OG-format shares (1200×630) are designed for link previews.
The share URL (/r/:shareId) serves OG meta tags to crawlers
and redirects browsers to the destinationUrl:
User shares link → platform crawler fetches /r/:shareId
→ gets OG image + meta → link preview renders in feed
Story-format shares (1080×1920) are designed for **direct image
sharing** via the native OS share sheet. The rendered PNG is the
content — no link preview involved.
App fetches rendered PNG → triggers native share tray
→ user picks Instagram / WhatsApp / Snapchat
→ image opens in Stories composer
Client-side implementation (Web Share API):
// 1. Create the share with story format
const res = await fetch('/v1/shares', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders },
body: JSON.stringify({
templateId: '...',
payload: { playerName: 'Ronaldo', score: '14' },
destinationUrl: 'https://myapp.com/fan/42',
format: 'story'
})
});
const { shareId, previewUrl } = await res.json();
// 2. Poll until ready (same as OG)
// ... poll GET /v1/shares/:shareId until status === "ready"
// 3. Fetch the rendered image as a file
const imgRes = await fetch(previewUrl);
const blob = await imgRes.blob();
const file = new File([blob], 'story.png', { type: 'image/png' });
// 4. Trigger native share tray
if (navigator.canShare?.({ files: [file] })) {
await navigator.share({
files: [file],
title: 'My Score Card',
url: \`https://hypecard.io/r/\${shareId}\`
});
} else {
// Fallback: download the image
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'story.png';
a.click();
}
Platform behaviour:
| Platform | What happens |
|---|---|
| Opens Stories composer with 9:16 image pre-loaded | |
| Shares as Status update or direct message image | |
| Snapchat | Opens in Snap editor |
| TikTok | Image available as background for new post |
| Fallback | Downloads PNG for manual sharing |
Notes:
navigator.share with files) works on mobile Safari (iOS 15+) and Chrome AndroiddestinationUrl still matters for deep-linking if the card includes a QR code or branded URLAll /v1/ endpoints require two headers:
| Header | Example | Required |
|---|---|---|
x-organisation-id | org-001 | Yes |
Authorization | Bearer dev-token | Yes |
Public endpoints (/r/:shareId, /m/:shareId/:kind, /health,
/docs) require no authentication.
Remote Model Context Protocol server — call any tool via HTTP POST.
POST /mcp
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"<tool>","arguments":{...}}}
List all tools:
curl -X POST /mcp -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Default org for MCP calls: org-001 (override via
MCP_DEFAULT_ORG env var or pass organisationId per call).
List all HTML share-card templates for an organisation.
System templates (built-in) are always included.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| organisationId | string | No | Defaults to org-001 |
Create a named template container. Returns templateId needed for
add_template_version.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Human-readable name |
| organisationId | string | No | Defaults to org-001 |
Add an HTML version to a template. Version numbers increment
automatically (v1, v2, …). Returns versionId.
The format field determines render dimensions:
og (default) → 1200×630 px (Open Graph)story → 1080×1920 px (9:16 portrait)A single template can hold versions of any format. Use Handlebars
{{variableName}} tokens for dynamic content. Inline all CSS —
Google Fonts are supported but slow (~30s render).
Parameters
| Name | Type | Required | Description | ||
|---|---|---|---|---|---|
| templateId | string | Yes | From create_template | ||
| format | og \ | story | No | Default: og | |
| html | string | Yes | Full HTML with {{var}} tokens | ||
| styles | string | No | Extra CSS injected into <head> | ||
| previewType | image \ | video \ | both | No | Default: image |
| variables | VariableSpec[] | No | Variable declarations |
VariableSpec
{ "name": "playerName", "required": true, "defaultValue": "A fan" }
Handlebars notes
{{name}}, {{score}} rankSuffix: "st" rather than computing it in the template
Get a template and its latest HTML version.
Parameters
| Name | Type | Required |
|---|---|---|
| templateId | string | Yes |
Download the raw HTML source of a template version for local editing.
Edit the HTML, then call publish_template_version to commit it.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| templateId | string | Yes | |
| versionId | string | No | Specific version ID; omit for latest |
Returns { versionId, templateId, version, format, html, styles, variables, createdAt }
Commit edited HTML as a new version of a template (the "save" step).
Variables are inherited from the previous version unless supplied.
Returns the new versionId.
Parameters
| Name | Type | Required | Description | |
|---|---|---|---|---|
| templateId | string | Yes | ||
| format | og \ | story | No | Inherited from previous version |
| html | string | Yes | Full edited HTML | |
| styles | string | No | Optional extra CSS | |
| variables | VariableSpec[] | No | Omit to inherit |
Typical local-edit workflow (MCP)
1. list_templates → find templateId
2. get_template_html(templateId) → copy html field
3. edit HTML locally or ask the LLM to modify it
4. publish_template_version(templateId, html) → new versionId
5. create_share(templateId, format, payload, destinationUrl)
6. poll get_share_status until status = "ready"
7. share the shareUrl
Create a share from a template. Rendering is async — poll
get_share_status until status is "ready".
The format field selects which template format to render:
og (default) → uses the latest OG (1200×630) versionstory → uses the latest story (1080×1920) versionIf no version of the requested format exists, returns 400.
Parameters
| Name | Type | Required | Description | |
|---|---|---|---|---|
| templateId | string | Yes | ||
| format | og \ | story | No | Default: og |
| payload | object | Yes | Key/value data for Handlebars tokens | |
| destinationUrl | string | Yes | Where humans navigate | |
| templateVersionId | string | No | Pin to a specific version | |
| organisationId | string | No | Defaults to org-001 |
Returns { shareId, shareUrl, previewUrl, status, format, expiresAt }
Check the render status of a share. Poll until status === "ready".
Parameters
| Name | Type | Required |
|---|---|---|
| shareId | string | Yes |
Status values
| Status | Meaning |
|---|---|
pending | Queued for render |
rendering | Rendering in progress |
ready | Render complete — image available |
failed | All retries exhausted |
Returns { shareId, status, format, shareUrl, previewImageUrl, expiresAt }
Once ready, previewImageUrl serves the rendered PNG directly
(dimensions match the share's format).
Delete a share permanently.
Parameters
| Name | Type | Required |
|---|---|---|
| shareId | string | Yes |
Return this API reference as Markdown.
GET /health
Returns {"status":"OK"}. No auth required.
POST /v1/templates
Body: { name, organisationId?, spaceId?, projectId? }
Returns: Template
POST /v1/templates/:templateId/versions
Body: { format?, html, styles?, previewType?, variables? }
Returns: TemplateVersion (201)
GET /v1/templates
Returns: { items: Template[] } (includes system templates)
GET /v1/templates/:templateId/versions/:versionId
versionId can be a UUID or the string "latest"
Returns: TemplateVersion (full JSON including html + styles)
GET /v1/templates/:templateId/versions/:versionId?format=html
Returns: raw HTML (text/html) with Content-Disposition header
→ pipe directly to a file: curl "..." > template.html
POST /v1/shares
Body: { templateId, format?, payload, destinationUrl,
templateVersionId?, attributionData?, expirySeconds? }
Returns: { shareId, shareUrl, previewUrl, status, format }
GET /v1/shares/:shareId
Returns: Share (with status and format)
POST /v1/shares/:shareId/render
Force a re-render (e.g. after template edit)
Returns: 202 Accepted
DELETE /v1/shares/:shareId
Returns: 204 No Content
GET /r/:shareId
Crawler UA (Twitterbot, facebookexternalhit, etc.)
→ returns OG meta HTML with og:image pointing to rendered PNG
(dimensions match share format)
Human UA (browser)
→ 302 redirect to the share's destinationUrl
(with utm_source and hc_share_id attribution params)
GET /m/:shareId/:kind (kind = image | video | thumbnail)
image → rendered PNG (1200×630 for OG, 1080×1920 for story)
video → MP4 preview
thumbnail → JPEG thumbnail (640×320)
Returns 202 + Retry-After if render not yet complete
POST /v1/assets (multipart/form-data, field name = "file")
Returns: { id, url, mimeType, filename, size }
GET /v1/assets
Returns: { items: Asset[] }
GET /v1/settings/effective?orgId=&spaceId=&projectId=
Returns: merged settings for the given scope hierarchy
Built-in templates are available to all organisations
(organisationId _system). They appear at the top of
list_templates results. Each has both OG and story versions.
Dark purple gradient
playerName\*, score\*, event (default: "Final Score")Dark minimal
homeTeam\*, awayTeam\*, prediction\*, author (default: "A fan")Gold-border card
playerName\*, achievement\*, description (default: "Keep it up!"), icon (default: trophy)Minimal dark
playerName\*, rank\*, rankSuffix (default: "th"), score (default: "0"), above (default: "0"), below (default: "0"), leaderboard (default: "Leaderboard")\* required
width:1200px; height:630px on bodystyles field for a <style> blockoverflow:hidden on body to clip content to canvaswidth:1080px; height:1920px on body GET /m/:shareId/image
HypeCard uses Satori for fast, in-process image rendering (~50-150ms).
Satori converts HTML to SVG, then to PNG — it does not run a browser.
This means templates must follow these rules:
MUST:
style="..." on every element. Satori ignores <style> blocks and CSS class selectors entirely.
display:flex to every <div> — Satori requires an explicitdisplay property on all elements. The render engine auto-injects
display:flex on divs that lack it, but being explicit is safer.
<body> — width and height must match theformat (1200x630 for OG, 1080x1920 for story).
display:flex and display:none. Grid, table, block, and inline-block are not supported.
MUST NOT:
.card { ... }) — styles will be silentlydropped and elements will render unstyled
display:block, display:inline-block, or display:table
::before, ::after)<img> tags with relative URLs (use absolute URLs or data URIs)as text nodes which break layout)
Supported CSS (subset):
flex-direction, align-items, justify-content, flex-wrap, gap, flex, flex-shrink, flex-grow
width, height, padding, margin, border, border-radius, overflow
font-size, font-weight, font-family, line-height, letter-spacing, text-align,
text-transform, color
background, opacity, box-shadowposition (absolute/relative), top, right, bottom, left
Example — correct Satori-compatible template:
<body>
<div style="display:flex;flex-direction:column;align-items:center;">
<div style="font-size:64px;font-weight:900;">{{playerName}}</div>
<div style="font-size:130px;color:#a78bfa;">{{score}}</div>
</div>
</body>
| Platform | Mechanism | Format | Status |
|---|---|---|---|
| Twitter/X | URL unfurl | og | Works |
| URL unfurl | og | Works | |
| URL unfurl | og | Works | |
| WhatsApp DMs | Link preview | og | Works |
| iMessage | Apple Rich Link | og | Works |
| Telegram | URL unfurl | og | Works |
| Instagram Stories | Image upload (9:16) | story | Via share page |
| TikTok | Image upload (9:16) | story | Via share page |
| Snapchat | Image upload (9:16) | story | Via share page |
| WhatsApp Status | Image upload | story | Via share page |
export API="http://localhost:3001"
export ORG="org-001"
export TOKEN="dev-token"
# 1. List templates — note the templateId
curl -s "$API/v1/templates" \
-H "x-organisation-id: $ORG" \
-H "Authorization: Bearer $TOKEN" | jq
# 2. Download latest HTML to a local file
export TEMPLATE_ID="<id from above>"
curl -s "$API/v1/templates/$TEMPLATE_ID/versions/latest?format=html" \
-H "x-organisation-id: $ORG" \
-H "Authorization: Bearer $TOKEN" \
> template.html
# 3. Edit template.html, then commit
curl -s -X POST "$API/v1/templates/$TEMPLATE_ID/versions" \
-H "x-organisation-id: $ORG" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"html\": $(jq -Rs . < template.html)}" | jq
# 4. Create a story share
curl -s -X POST "$API/v1/shares" \
-H "x-organisation-id: $ORG" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"templateId":"'"$TEMPLATE_ID"'",
"format":"story",
"payload":{"playerName":"Alice","score":"42"},
"destinationUrl":"https://example.com"}' | jq
# 5. Poll status
curl -s "$API/v1/shares/$SHARE_ID" \
-H "x-organisation-id: $ORG" \
-H "Authorization: Bearer $TOKEN" | jq .status
When a template version has previewType: "video" or "both", the
platform generates an animated H.264 MP4 in addition to the static PNG.
The render pipeline uses a hybrid approach:
1. Satori renders the template to a high-quality PNG (~100ms)
2. Remotion animates that PNG with a fade-in + zoom entrance
(frames 0–20: opacity 0→1, scale 0.92→1.0) and encodes H.264 MP4
3. FFmpeg extracts a mid-video frame as a JPEG thumbnail
The result is a 4-second MP4 of the exact same card design — no separate
video template needed.
# GET /m/:shareId/video → MP4 (ready after render completes)
curl -s -L -o card.mp4 "$API/m/$SHARE_ID/video"
# GET /m/:shareId/thumbnail → JPEG thumbnail (640×320)
curl -s -L -o thumb.jpg "$API/m/$SHARE_ID/thumbnail"
Returns 202 + Retry-After if the render is still in progress.
Set previewType when creating a template version:
| Value | Image | Video | Thumbnail |
|---|---|---|---|
image (default) | ✅ | ✗ | ✗ |
video | ✗ | ✅ | ✅ |
both | ✅ | ✅ | ✅ |
# Create a version with video enabled
curl -X POST "$API/v1/templates/$TEMPLATE_ID/versions" \
-H "x-organisation-id: $ORG" -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"html":"...","previewType":"both","format":"og"}'
Video generation requires RENDER_WORKER=remotion. The Render Lambda
must have ≥2048MB memory and ≥120s timeout.
Typical video render time on Lambda: 25-45s (includes Chromium cold start).
| Render type | Typical time |
|---|---|
| Image — inline styles (Satori) | 50–150ms |
| Image — Google Fonts (Satori) | 2–5s |
| Video — Remotion MP4 (warm) | 15–25s |
| Video — Remotion MP4 (cold start) | 25–45s |
| Failed (all retries exhausted) | ~6s total (3 retries: 2s + 4s backoff) |