Masky Developer API
Drive Masky — the AI avatar platform — over HTTP. Generate images, create avatars (image + personality + voice), run conversations that render spoken audio and talking-head video, embed an auto-playing live viewer, and add “Login with Masky” SSO to your own site.
curl.Overview
Base URL: https://masky.ai/api. All request and response bodies are JSON (Content-Type: application/json). Avatars each have a still image, a personality prompt, and a voice; you start a conversation with one and inject turns, each of which renders audio and optional video. Conversations and individual turns expose embeddable live-viewer URLs.
Authentication
Get a key at masky.ai/developer (sign in → Generate key → copy the mky_… token, shown once). Send it on every request:
Authorization: Bearer mky_...
Access tokens minted through Login with Masky are also mky_ keys and work on these same endpoints, limited to their granted scopes. The avatar owner is billed for generation — when a token renders someone else's publicly-renderable avatar, the owner pays, not the caller.
Conventions & credits
- Async turns. A conversation turn returns immediately with a
pendingshell; audio/video URLs land on the same turn doc as the worker finishes (poll, or just embed the live URL). Text ≈ 3s, audio ≈ 5–10s, video ≈ 30–60s. - Auto-chunking. Long replies split into ~3-sentence pieces; each becomes its own turn doc and the live URL plays them seamlessly in order.
- Failures are free. Image and render charges apply only on success.
- 1 credit = $1. Spoken turns (audio / talking-head video) are billed per second of speech, estimated from the text — so a cost can be quoted before rendering.
| Action | Credits |
|---|---|
Image generate (/images/generate) | 0.005 |
Image edit (/images/edit) | 0.01 |
| Spoken audio (per second) | ~0.0015 / sec |
| Talking-head video (per second of speech) | ~0.0265 / sec |
| Video generation (per second of output) | ~0.02–0.03 / sec |
POST/images/generate
Text-to-image. Synchronous — returns when rendered & stored. Requires the generate scope for SSO tokens.
curl -s -X POST https://masky.ai/api/images/generate \
-H "Authorization: Bearer $MASKY_API_KEY" -H "Content-Type: application/json" \
-d '{"prompt":"photorealistic portrait of a friendly robot barista","aspectRatio":"1:1"}'
# 200 -> { "imageUrl": "https://.../img.jpeg", "aspectRatio": "1:1", "creditCost": 0.005 }
| Field | |
|---|---|
prompt | required, ≤ 2000 chars. |
aspectRatio | 1:1 (default), 16:9, 9:16, 4:3, 3:4, 3:2, 2:3. |
The returned imageUrl is a long-lived public URL — pass it straight into create avatar.
POST/images/edit
Edit/compose from 1–5 reference images. Requires the generate scope for SSO tokens.
curl -s -X POST https://masky.ai/api/images/edit \
-H "Authorization: Bearer $MASKY_API_KEY" -H "Content-Type: application/json" \
-d '{"prompt":"put her in a spacesuit","imageUrl":"https://.../portrait.jpeg"}'
# 200 -> { "imageUrl": "https://.../edited.jpeg", "aspectRatio": "match_input_image", "creditCost": 0.01 }
prompt | required, the edit instruction. |
imageUrl / images[] | one URL, or an array of up to 5 http(s) URLs (required). |
aspectRatio | defaults to match_input_image; or any value from generate. |
POST/videos/generate
Render a short video via Masky's video models — text-to-video, image-to-video, or video editing. Async (renders take ~1–3 min): this returns 202 with a generationId; poll GET /videos/{id} until status is video. Requires the generate scope for SSO tokens. Charges 1 credit on success.
Avatar-aware. Reference your avatars by name in the prompt (or pass avatarIds) and their images are sent to an image-conditioned model so they actually appear in the video.
curl -s -X POST https://masky.ai/api/videos/generate \
-H "Authorization: Bearer $MASKY_API_KEY" -H "Content-Type: application/json" \
-d '{"prompt":"Aphrodite walks along a moonlit beach, camera slowly pushing in","resolution":"720p"}'
# 202 -> { "generationId":"…", "status":"pending", "model":"cinematic",
# "referencedAvatars":[{"id":"…","name":"Aphrodite"}], "poll":"/api/videos/…" }
prompt | required. Mention avatars by name to feature them. |
avatarIds | optional explicit avatars to feature (up to 3), added to any named in the prompt. |
image | optional explicit conditioning image URL (image-to-video). |
srcVideo | optional source video URL to edit (uses ensemble) — combine with a prompt describing the change. |
model | override the auto-pick: cinematic (default; text→video + single-image→video), motion (image→video), scene (text→video), ensemble (1–3 reference images for character consistency, or video editing). |
resolution, aspectRatio, duration, seed | passed through (model-specific; e.g. cinematic resolution is 720p or 1080p). |
Model auto-pick: 0–1 reference image → cinematic; 2–3 → ensemble (character consistency); a srcVideo → ensemble (video-to-video edit).
GET/videos/{generationId}
Poll a video job. { status: "pending" | "video" | "error", videoUrl, model }. videoUrl is a long-lived public URL once status is video.
curl -s https://masky.ai/api/videos/$ID -H "Authorization: Bearer $MASKY_API_KEY"
# -> { "status":"pending" } …then… { "status":"video", "videoUrl":"https://…/vid.mp4" }
GET/avatars
List the avatars you own. GET /avatars/public lists avatars others have marked publicly renderable.
GET/avatars/{avatarId}/images
The avatar's still-image variants — primary portrait plus every stored asset. Use this to let a user pick the avatar's look before rendering instead of generating a new image. Own avatars always; someone else's via ?avatarOwnerUserId= when the avatar is publicly renderable. SSO tokens need avatars:read or generate.
curl -s https://masky.ai/api/avatars/$AVATAR_ID/images -H "Authorization: Bearer $TOKEN"
# 200 -> { avatarId, avatarOwnerUserId, primary,
# images: [ { url, assetId, isPrimary, createdAt } ] }
Any returned url is valid as the avatarImageUrl override on /avatars/{avatarId}/speak, or on POST/conversations/{conversationId}/avatar-image { avatarImageUrl } (owner only) to switch the look used by a conversation's later video turns.
POST/avatars
Create an avatar. The image URL is fetched and stored by Masky — supply a public URL (e.g. the imageUrl from /images/generate).
curl -s -X POST https://masky.ai/api/avatars \
-H "Authorization: Bearer $MASKY_API_KEY" -H "Content-Type: application/json" \
-d '{"displayName":"Dragon","imageUrl":"https://.../dragon.jpeg",
"personalityPrompt":"A wise, theatrical dragon.","voiceId":""}'
# 200 -> { "avatarId": "...", "avatarOwnerUserId": "..." }
displayName | required. |
imageUrl | public image URL (fetched + stored). |
personalityPrompt | character spec used in chat mode. |
voiceId | from /voices. |
knowledgeBaseUrls, publiclyRenderable | optional. |
GET/voices
Preset voices plus any custom voices you already have. Call before create/set-voice. Attach one with POST/avatars/{avatarId}/voice { voiceId }. Cloning a custom voice from audio samples is done in the web UI; cloned voices then appear here.
POST/avatars/{avatarId}/public
{ publiclyRenderable: true|false } — toggle whether other developers can render this avatar (you pay the credits when they do).
POST/avatars/{avatarId}/speak
One-shot generation — a single TTS or talking-head clip with no conversation thread. Async: returns a generation you poll with GET/avatars/speak/{generationId} (status pending → audio → video or error; includes audioUrl, videoUrl, shareSlug).
{ "text": "Hello!", "textMode": "literal", "output": "video" }
text | required, ≤ 500 chars. |
textMode | literal (default) or personality (rewrites via the avatar's prompt). |
output | audio (default) or video. |
avatarImageUrl | optional still to render with — must be a url from /avatars/{avatarId}/images (400 otherwise). Default: the avatar's primary image. |
avatarOwnerUserId | required when rendering someone else's publicly renderable avatar. |
Identical requests are deduplicated (HTTP 200 reused:true, no double charge). Public play link: GET/g-YY-MM-XXXX 302s to a signed URL.
POST/conversations
Start a conversation with an avatar.
curl -s -X POST https://masky.ai/api/conversations \
-H "Authorization: Bearer $MASKY_API_KEY" -H "Content-Type: application/json" \
-d '{"avatarId":"...","avatarOwnerUserId":"..."}'
# 200 -> { conversationId, shareSlug, viewerToken, liveUrl }
liveUrl (https://masky.ai/live/c-…?token=…) is the embeddable viewer — avatar image fills the window, audio/video overlays on top, and each new turn auto-plays. Drop it in an OBS Browser Source or any web page.
POST/conversations/{conversationId}/turn
Inject a turn; the avatar replies asynchronously. Requires the generate scope for SSO tokens.
curl -s -X POST https://masky.ai/api/conversations/$ID/turn \
-H "Authorization: Bearer $MASKY_API_KEY" -H "Content-Type: application/json" \
-d '{"userText":"Tell me about Masky.","output":"video","mode":"chat"}'
userText | required — what is said to the avatar. |
output | text | audio | video (default audio). No-voice avatars soft-degrade to text. |
mode | chat (default; Gemini replies using the avatar's personality + history) or speak (says userText verbatim). |
reinterpret | in speak mode, rewrite userText through the avatar's personality before TTS. |
The conversation accepts the internal id or the c-… share slug in the path. Conversation is the caller's (or its owner's) — turns spend the owner's credits.
Two-avatar turns
Render both sides of an exchange in one call — a second avatar of yours speaks the user line, the conversation avatar speaks a scripted reply:
{
"userText": "So what is this, in one breath?",
"avatarText": "An API that turns text into avatars who talk.",
"speakerAvatarId": "<your second avatarId>",
"userOutput": "video",
"output": "video",
"mode": "chat"
}
speakerAvatarId | render the user turn as a second avatar you own (its face + voice). |
userOutput | text | audio | video for that speaker turn. |
avatarText | exact line the conversation avatar speaks (skips Gemini). |
GET/conversations/by-slug/{slug}
Returns the conversation and all turns with freshly re-signed media. Poll your turn by id — done when status=="ready" (text), audioUrl set (audio), or videoUrl set (video); status=="error" + errorMessage on failure. Once a turn has media it also carries:
shareSlug | t-YY-MM-XXXX. |
shareUrl | https://masky.ai/t-… — a static social-card page that plays the clip. |
liveUrl | https://masky.ai/live/t-… — the live viewer scoped to that one turn (play/pause, sound + volume, caption translation). No token needed; the slug is the grant. |
POST/conversations/{conversationId}/visibility
{ isPublic: true|false }. Public conversations are readable by slug without auth; private ones require the owner.
Live viewer URLs
- Whole conversation — the conversation's
liveUrl:https://masky.ai/live/c-YY-MM-XXXX?token=…. Plays every turn in order; append?since=now(or#new) to play only turns created after load. - Single turn — a turn's
liveUrl:https://masky.ai/live/t-YY-MM-XXXX. Same UI, just that one turn.
Both pages support play/pause, sound + a volume slider, and live caption translation into any language. The ?token= on a conversation live URL is the access grant — treat it like a secret for private conversations.
| Query param | |
|---|---|
autoplay=0 | load paused — nothing plays until the viewer presses Play. Default: playback starts on load (muted if the browser blocks sound-on autoplay). |
since=now | play only turns created after page load (also #new); accepts an epoch-ms or ISO date for an explicit cutoff. |
Embedding in your site
<iframe src="https://masky.ai/live/c-YY-MM-XXXX?token=…"
allowfullscreen allow="fullscreen"
style="border:0;width:640px;height:360px"></iframe>
The viewer shows a fullscreen button (top-right) that expands the embed to fullscreen so viewers can watch and use the controls at full size. The button only appears when fullscreen is actually permitted: on a top-level page (OBS Browser Sources, direct links) it always shows; inside an iframe the browser exposes it only when your <iframe> tag grants it via allowfullscreen / allow="fullscreen" (include both — older browsers read the attribute, newer ones the permissions policy). Omit them and the button simply doesn't render.
Tribes
Every route requires auth (Firebase session, API key, or an SSO token with the tribes scope). {user} is a Masky uid or a twitchUsername. These are not public directories: membership questions are answered only for your own tribe or your own membership.
# Tribe info + whether YOU are a member
curl -s https://masky.ai/api/tribes/oceanseth -H "Authorization: Bearer $TOKEN"
# 200 -> { user, tribeName, joinCost, memberCount, isMember, isOwner }
# Is a specific user in YOUR tribe? (or omit member= to ask about yourself)
curl -s "https://masky.ai/api/tribes/<you>/membership?member=someviewer" -H "Authorization: Bearer $TOKEN"
# 200 -> { tribeOwner, member, joinedAt }
# Join a tribe — charges the creator's joinCost in YOUR credits
# (free if you're an active Twitch sub of the creator)
curl -s -X POST https://masky.ai/api/tribes/oceanseth/join -H "Authorization: Bearer $TOKEN"
# 200 -> { success, isSubscriber, charged } 400 -> { error: "Insufficient balance", balance, required }
Credit gifts (user → user donations)
Send Masky credits from the signed-in user to another user. Two-phase for safety: your app proposes a gift with an SSO token (donations scope), then opens the returned confirmUrl — a masky.ai page — in a popup where the user confirms with their own Masky session. Delegated tokens can never execute a transfer; only the confirm page can. Intents expire after 10 minutes.
# 1) Propose (SSO token, `donations` scope)
curl -s -X POST https://masky.ai/api/donations/intents \
-H "Authorization: Bearer $SSO_TOKEN" -H "Content-Type: application/json" \
-d '{"to":"oceanseth","amount":5,"note":"great stream!"}'
# 200 -> { intentId, status:"pending", confirmUrl, expiresAt, ... }
# 2) Open confirmUrl in a popup; the user clicks "Send credits" there.
# The page postMessages {type:"masky:gift", status, intentId} to the opener
# (treat as a hint) — poll for the authoritative status:
curl -s https://masky.ai/api/donations/intents/$INTENT_ID -H "Authorization: Bearer $SSO_TOKEN"
# status: pending | confirmed | cancelled | expired
# 3) Gifts you've sent to a user (recognition / totals)
curl -s "https://masky.ai/api/donations/sent?to=oceanseth" -H "Authorization: Bearer $SSO_TOKEN"
# 200 -> { to, total, count, gifts:[{amount, note, createdAt}] }
Gifted credits arrive as global Masky credits (spendable anywhere). The transferable balance excludes credits locked to a creator's page or already spent, so a proposal can fail with Insufficient transferable credits even when a page-specific balance looks larger.
Share links
GET /g-YY-MM-XXXX (one-shot speak) and GET /t-YY-MM-XXXX (conversation turn) are public, no-auth links that 302 to a freshly-signed media URL and render a social card. Year/month are UTC; the code is four lowercase letters/digits.
Login with Masky (SSO)
Masky is an OAuth 2.0 authorization-code identity provider. The twist: the user picks which of their avatars represents them on your site — the avatar's name + photo become the identity you receive, and with the generate scope you can call this API as that avatar, on their credits.
| Scope | |
|---|---|
profile | identity (name, picture, pseudonymous sub) via /userinfo. |
avatars:read | list the user's avatars. |
generate | spend the user's credits on images / speak / turns. |
tribes | tribe info + membership queries; join a tribe (fee in the user's credits). |
donations | propose credit gifts (user confirms on masky.ai) + list gifts sent. |
POST/oauth/clients first-party key
Register your app (also available as the MCP tool register_oauth_client). Authenticate with your own mky_ key.
curl -s -X POST https://masky.ai/api/oauth/clients \
-H "Authorization: Bearer $MASKY_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"My App","redirectDomains":["myapp.com"],"scopes":["profile","generate"]}'
# -> { "clientId":"mkc_…", "clientSecret":"mks_…", ... } # secret shown ONCE
GET /oauth/clients lists your apps; DELETE /oauth/clients/{clientId} removes one.
Authorize (consent)
Send the user to the consent screen. redirect_uri's host must be in your redirectDomains (https only, except localhost). PKCE (code_challenge/S256) is supported for public clients.
https://masky.ai/oauth-authorize.html?client_id=mkc_…&redirect_uri=https://myapp.com/callback&scope=profile%20generate&state=<csrf>
They approve and Masky redirects to redirect_uri?code=…&state=….
POST/oauth/token
curl -s -X POST https://masky.ai/api/oauth/token \
-H "Content-Type: application/json" \
-d '{"grant_type":"authorization_code","code":"<code>",
"redirect_uri":"https://myapp.com/callback",
"client_id":"mkc_…","client_secret":"mks_…"}'
# -> { "access_token":"mky_…", "token_type":"Bearer",
# "scope":"profile generate", "avatar":{ "id","name","picture" } }
The access_token is a scoped mky_ key — use it as Authorization: Bearer on any endpoint here (subject to its scopes). Auth codes are single-use and expire in 5 minutes.
GET/oauth/userinfo
curl -s https://masky.ai/api/oauth/userinfo -H "Authorization: Bearer <access_token>"
# -> { "sub":"ava_…", "name":"Aphrodite", "picture":"https://…",
# "avatar_id":"…", "scope":"profile generate" }
sub is a stable pseudonym per (user, your app, avatar) — never the raw Masky uid, and un-correlatable across sites. Treat it as the user's account id on your side.
Connected apps
A user can review and revoke authorized apps at masky.ai/developer. Programmatically: GET/oauth/grants lists a user's granted tokens (first-party key) and DELETE/oauth/grants/{id} revokes one.
OpenAPI
Machine-readable specs: masky-openapi.json (full API) and openapi.json (avatar-speak). Load either into Postman, Swagger UI, or a codegen tool.