HypeCard API Reference

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.


Template Formats

Each template version has a format field:

FormatDimensionsUse Case
og1200×630Open Graph images (default)
story1080×1920Instagram 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.


Sharing Flows

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

Stories (native share tray)

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:

PlatformWhat happens
InstagramOpens Stories composer with 9:16 image pre-loaded
WhatsAppShares as Status update or direct message image
SnapchatOpens in Snap editor
TikTokImage available as background for new post
FallbackDownloads PNG for manual sharing

Notes:


Authentication (REST)

All /v1/ endpoints require two headers:

HeaderExampleRequired
x-organisation-idorg-001Yes
AuthorizationBearer dev-tokenYes

Public endpoints (/r/:shareId, /m/:shareId/:kind, /health,

/docs) require no authentication.


MCP Endpoint

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).


MCP Tools

list_templates

List all HTML share-card templates for an organisation.

System templates (built-in) are always included.

Parameters

NameTypeRequiredDescription
organisationIdstringNoDefaults to org-001

create_template

Create a named template container. Returns templateId needed for

add_template_version.

Parameters

NameTypeRequiredDescription
namestringYesHuman-readable name
organisationIdstringNoDefaults to org-001

add_template_version

Add an HTML version to a template. Version numbers increment

automatically (v1, v2, …). Returns versionId.

The format field determines render dimensions:

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

NameTypeRequiredDescription
templateIdstringYesFrom create_template
formatog \storyNoDefault: og
htmlstringYesFull HTML with {{var}} tokens
stylesstringNoExtra CSS injected into <head>
previewTypeimage \video \bothNoDefault: image
variablesVariableSpec[]NoVariable declarations

VariableSpec


{ "name": "playerName", "required": true, "defaultValue": "A fan" }

Handlebars notes

rankSuffix: "st" rather than computing it in the template


get_template

Get a template and its latest HTML version.

Parameters

NameTypeRequired
templateIdstringYes

get_template_html

Download the raw HTML source of a template version for local editing.

Edit the HTML, then call publish_template_version to commit it.

Parameters

NameTypeRequiredDescription
templateIdstringYes
versionIdstringNoSpecific version ID; omit for latest

Returns { versionId, templateId, version, format, html, styles, variables, createdAt }


publish_template_version

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

NameTypeRequiredDescription
templateIdstringYes
formatog \storyNoInherited from previous version
htmlstringYesFull edited HTML
stylesstringNoOptional extra CSS
variablesVariableSpec[]NoOmit 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_share

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:

If no version of the requested format exists, returns 400.

Parameters

NameTypeRequiredDescription
templateIdstringYes
formatog \storyNoDefault: og
payloadobjectYesKey/value data for Handlebars tokens
destinationUrlstringYesWhere humans navigate
templateVersionIdstringNoPin to a specific version
organisationIdstringNoDefaults to org-001

Returns { shareId, shareUrl, previewUrl, status, format, expiresAt }


get_share_status

Check the render status of a share. Poll until status === "ready".

Parameters

NameTypeRequired
shareIdstringYes

Status values

StatusMeaning
pendingQueued for render
renderingRendering in progress
readyRender complete — image available
failedAll retries exhausted

Returns { shareId, status, format, shareUrl, previewImageUrl, expiresAt }

Once ready, previewImageUrl serves the rendered PNG directly

(dimensions match the share's format).


delete_share

Delete a share permanently.

Parameters

NameTypeRequired
shareIdstringYes

get_docs

Return this API reference as Markdown.


REST Endpoints

Health


GET /health

Returns {"status":"OK"}. No auth required.


Templates


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

Shares


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

Public (no auth)


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

Assets


POST /v1/assets   (multipart/form-data, field name = "file")
Returns: { id, url, mimeType, filename, size }

GET /v1/assets
Returns: { items: Asset[] }

Settings


GET /v1/settings/effective?orgId=&spaceId=&projectId=
Returns: merged settings for the given scope hierarchy

System Templates

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.

Score Card

Dark purple gradient

Match Prediction

Dark minimal

Achievement Unlocked

Gold-border card

Leaderboard Rank

Minimal dark

\* required


Template Design Guidelines

OG image (format: "og")

Story image (format: "story")

GET /m/:shareId/image

Satori Compatibility Rules

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:

ignores <style> blocks and CSS class selectors entirely.

display property on all elements. The render engine auto-injects

display:flex on divs that lack it, but being explicit is safer.

format (1200x630 for OG, 1080x1920 for story).

display:none. Grid, table, block, and inline-block are not supported.

MUST NOT:

dropped and elements will render unstyled

display:table

as text nodes which break layout)

Supported CSS (subset):

flex-wrap, gap, flex, flex-shrink, flex-grow

border-radius, overflow

line-height, letter-spacing, text-align,

text-transform, color

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 support matrix

PlatformMechanismFormatStatus
Twitter/XURL unfurlogWorks
LinkedInURL unfurlogWorks
FacebookURL unfurlogWorks
WhatsApp DMsLink previewogWorks
iMessageApple Rich LinkogWorks
TelegramURL unfurlogWorks
Instagram StoriesImage upload (9:16)storyVia share page
TikTokImage upload (9:16)storyVia share page
SnapchatImage upload (9:16)storyVia share page
WhatsApp StatusImage uploadstoryVia share page

Download / Upload Workflow (REST)


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

Video Previews

When a template version has previewType: "video" or "both", the

platform generates an animated H.264 MP4 in addition to the static PNG.

How it works

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.

Fetching the video


# 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.

Template configuration

Set previewType when creating a template version:

ValueImageVideoThumbnail
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"}'

Requirements

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 Timing

Render typeTypical 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)