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 (ideogram model: 0.015) |
Image edit (/images/edit) | 0.01 (qwen model: 0.03) |
| 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. |
model | default (0.005 credits) or ideogram (0.015 credits) — renders legible in-image text/typography; use it whenever the prompt asks for readable words. If omitted, a prompt containing "double-quoted" text auto-routes to ideogram, and an unquoted prompt that clearly intends readable words is detected by a reasoning pass and routed the same way (the response reports the model and cost charged). |
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). The qwen model accepts at most 2. |
aspectRatio | defaults to match_input_image; or any value from generate. |
model | default (0.01 credits) or qwen (0.03 credits) — renders legible in-image text/typography; use it whenever the edit asks for readable words. If omitted, a prompt containing "double-quoted" text auto-routes to qwen when 1–2 reference images are given, and an unquoted prompt that clearly intends readable words is detected by a reasoning pass and routed the same way (the response reports the model and cost charged). |
POST/videos/generate
Render any video the prompt describes — text-to-video, or image-to-video from any public image or one of your avatars' stills — with sound. Not lip-synced (for talking to camera use a speak turn). Async (~40s–3min): returns 202 with a generationId; poll GET /videos/{id} until status is ready. Every render gets a /v-yy-mm-xxxx share page, private by default. Billed per second of the finished video, once, on success — rates from GET /videos/pricing (no auth). Requires the generate scope for SSO tokens.
curl -s -X POST https://masky.ai/api/videos/generate \
-H "Authorization: Bearer $MASKY_API_KEY" -H "Content-Type: application/json" \
-d '{"prompt":"slow push-in on a misty mountain lake at dawn, soft wind ambience","image":"https://example.com/lake.jpg","duration":5}'
# 202 -> { "generationId":"…", "slug":"v-26-09-ab12", "url":"https://masky.ai/v-26-09-ab12",
# "status":"queued", "seconds":5, "creditCost":0.125, "firestorePath":"videos/v-26-09-ab12", "poll":"/api/videos/…" }
prompt | required, ≤2000 chars. What happens in the shot and, without speechText/audio, what it sounds like. |
avatarId | optional, one of your avatars (404 otherwise). With image omitted the clip opens on its primary still. Required for speechText. |
image | opening frame, any public http(s) jpg/png/webp URL. Omit for the avatar's still; null for pure text-to-video. |
lastFrameImage | closing frame, any public image URL. |
speechText | ≤300 chars, spoken in the avatar's voice and used as the audio input (sets the length). Not with audio. |
audio | public flac/mp3/wav URL used as the audio input (sets the length). Not with speechText. |
duration | 1–20 seconds; omit to let the model choose. Ignored with speechText/audio. |
resolution | 720p (default) or 1080p. |
fps | 24 (default) or 48. |
aspectRatio | 16:9 (default), 9:16, 4:3, 3:4, 3:2, 2:3, 1:1. Ignored when an image is used. |
seed | integer for reproducible renders. |
draft | false default; true is lower quality and cheaper. |
saveAudio | true default; false renders a silent clip. |
promptUpsampling | true default (the model expands the prompt); false uses it verbatim. |
visibility | private (default) or public share page. |
Errors: 400 invalid field, 402 not enough credits ({ requiredCredits, availableCredits }), 403 missing scope, 404 avatar not found.
GET/videos/{generationId}
Poll a video job: { generationId, slug, url, status: "queued" | "rendering" | "ready" | "error", videoUrl, seconds, creditCost, error? }. videoUrl is a long-lived public MP4 URL once status is ready, and seconds/creditCost are then the billed values.
curl -s https://masky.ai/api/videos/$ID -H "Authorization: Bearer $MASKY_API_KEY"
# -> { "status":"rendering", … } …then… { "status":"ready", "videoUrl":"https://…/vid.mp4", "seconds":5.04, "creditCost":0.126, … }
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 users and apps can render this avatar (they spend their own 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. |
language | optional BCP-47-ish code (es, pt-br) declaring the language of text. The voice speaks whatever language the text is in — send text already in the target language; with textMode=personality the rewrite is produced in this language. |
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.