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 (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
promptrequired, ≤ 2000 chars.
aspectRatio1:1 (default), 16:9, 9:16, 4:3, 3:4, 3:2, 2:3.
modeldefault (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 }
promptrequired, 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.
aspectRatiodefaults to match_input_image; or any value from generate.
modeldefault (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/…" }
promptrequired, ≤2000 chars. What happens in the shot and, without speechText/audio, what it sounds like.
avatarIdoptional, one of your avatars (404 otherwise). With image omitted the clip opens on its primary still. Required for speechText.
imageopening frame, any public http(s) jpg/png/webp URL. Omit for the avatar's still; null for pure text-to-video.
lastFrameImageclosing 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.
audiopublic flac/mp3/wav URL used as the audio input (sets the length). Not with speechText.
duration1–20 seconds; omit to let the model choose. Ignored with speechText/audio.
resolution720p (default) or 1080p.
fps24 (default) or 48.
aspectRatio16:9 (default), 9:16, 4:3, 3:4, 3:2, 2:3, 1:1. Ignored when an image is used.
seedinteger for reproducible renders.
draftfalse default; true is lower quality and cheaper.
saveAudiotrue default; false renders a silent clip.
promptUpsamplingtrue default (the model expands the prompt); false uses it verbatim.
visibilityprivate (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": "..." }
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 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" }
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.
languageoptional 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"}'
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.