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.

Prefer turnkey tools? Install the Masky MCP server (same capabilities as native Claude tools), or hand any agent the Masky API skill — a single markdown file it can follow to call this API with 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

ActionCredits
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
promptrequired, ≤ 2000 chars.
aspectRatio1: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 }
promptrequired, the edit instruction.
imageUrl / images[]one URL, or an array of up to 5 http(s) URLs (required).
aspectRatiodefaults 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/…" }
promptrequired. Mention avatars by name to feature them.
avatarIdsoptional explicit avatars to feature (up to 3), added to any named in the prompt.
imageoptional explicit conditioning image URL (image-to-video).
srcVideooptional source video URL to edit (uses ensemble) — combine with a prompt describing the change.
modeloverride 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, seedpassed 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 srcVideoensemble (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": "..." }
displayNamerequired.
imageUrlpublic image URL (fetched + stored).
personalityPromptcharacter spec used in chat mode.
voiceIdfrom /voices.
knowledgeBaseUrls, publiclyRenderableoptional.

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" }
textrequired, ≤ 500 chars.
textModeliteral (default) or personality (rewrites via the avatar's prompt).
outputaudio (default) or video.
avatarImageUrloptional still to render with — must be a url from /avatars/{avatarId}/images (400 otherwise). Default: the avatar's primary image.
avatarOwnerUserIdrequired 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"}'
userTextrequired — what is said to the avatar.
outputtext | audio | video (default audio). No-voice avatars soft-degrade to text.
modechat (default; Gemini replies using the avatar's personality + history) or speak (says userText verbatim).
reinterpretin 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"
}
speakerAvatarIdrender the user turn as a second avatar you own (its face + voice).
userOutputtext | audio | video for that speaker turn.
avatarTextexact 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:

shareSlugt-YY-MM-XXXX.
shareUrlhttps://masky.ai/t-… — a static social-card page that plays the clip.
liveUrlhttps://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

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=0load paused — nothing plays until the viewer presses Play. Default: playback starts on load (muted if the browser blocks sound-on autoplay).
since=nowplay 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
profileidentity (name, picture, pseudonymous sub) via /userinfo.
avatars:readlist the user's avatars.
generatespend the user's credits on images / speak / turns.
tribestribe info + membership queries; join a tribe (fee in the user's credits).
donationspropose 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.