Skip to content
maskostudioDocs

All pages

  • Overview/docs/use-cases
  • Mobile app/docs/use-cases#mobile
  • Website/docs/use-cases#website
  • Desktop/docs/use-cases#desktop
  • AI agents/docs/use-cases#agents
  • Videos/docs/use-cases#videos
  • Brand/docs/use-cases#brand
  • Start building/docs/ai-agents
  • Set up once/docs/ai-agents#set-up-once
  • Ask in plain words/docs/ai-agents#ask-in-plain-words
  • What your agent does/docs/ai-agents#what-your-agent-does
  • MCP server/docs/mcp
  • Masko for Cursor/docs/ai-tools/cursor
  • Masko in ChatGPT/docs/ai-tools/chatgpt
  • Start here/docs/tutorials#start-here
  • All tutorials/docs/tutorials
  • Create your first mascot/docs/tutorials/first-mascot
  • Create an animated character/docs/tutorials/first-animation
  • Make a character talk/docs/tutorials/make-it-talk
  • Put your mascot on your website/docs/tutorials/mascot-on-your-website
  • Guide onboarding with a talking mascot/docs/tutorials/web-onboarding-mascot
  • Add a mascot that celebrates progress and welcomes users back/docs/tutorials/web-progress-companion
  • Add a talking support mascot in the bottom right/docs/tutorials/web-support-mascot
  • Show a mascot while your AI thinks/docs/tutorials/ai-loading-mascot
  • Give your AI agent a face/docs/tutorials/ai-agent-face
  • Animate the mascot you already have/docs/tutorials/animate-your-mascot
  • How to make Telegram stickers/docs/tutorials/telegram-stickers
  • How to make Discord stickers/docs/tutorials/discord-stickers
  • Quickstart: your first animation/docs/quickstart
  • Terminal quickstart/docs/terminal
  • API keys and workspaces/docs/authentication
  • CLI browser login/docs/authentication/cli
  • How mascots work/docs/how-mascots-work
  • Credits and costs/docs/credits
  • From text/docs/create/from-text
  • From an image/docs/create/from-image
  • From a website/docs/create/from-website
  • From a brand book/docs/create/from-brand-book
  • Reference images/docs/manage/references
  • What you can generate/docs/generation
  • Pose-to-animation walkthrough/docs/generate/workflow
  • Images and poses/docs/generate/images
  • Animations/docs/generate/animations
  • Talking animations/docs/generate/talking
  • Logos, backgrounds and stickers/docs/generate/design-assets
  • Cursor follower/docs/generate/cursor-follower
  • Batch generation/docs/generate/batch
  • Image and video exports/docs/manage/media-exports
  • Canvases and releases/docs/canvas
  • Build a canvas/docs/canvas/build
  • Canvas templates/docs/canvas/templates
  • Generate canvas assets/docs/canvas/generate-all
  • Export a canvas/docs/canvas/export
  • Overview and demo/docs/player
  • Play one animation/docs/player/one-animation
  • Play a canvas/docs/player/canvas
  • Hosting and file formats/docs/generate/cdn
  • File sizes/docs/generate/file-sizes
  • Float a mascot on your Mac/docs/integrations/desktop
  • Jobs and polling/docs/manage/jobs
  • Webhooks/docs/manage/webhooks
  • Manage mascots and assets/docs/manage/collections
  • All endpoints/docs/reference
  • Requests and responses/docs/reference/conventions
  • Errors and recovery/docs/reference/errors
  • Command line reference/docs/terminal/commands
  • TypeScript SDK/docs/sdk/typescript
  • Changelog/docs/reference/changelog
  • Migrate to mascots and canvases/docs/manage/studio-migration
Get an API key
Documentation

Webhooks

Register a URL and Masko POSTs job results to it when a job completes or fails, so you do not have to poll.

Create a Webhook

curl -X POST https://api.masko.ai/v1/webhooks \
  -H "Authorization: Bearer masko_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/api/masko-webhook",
    "events": ["job.completed", "job.failed"]
  }'

# Response:
# {
#   "data": {
#     "id": "wh_abc123",
#     "url": "https://your-app.com/api/masko-webhook",
#     "events": ["job.completed", "job.failed"],
#     "secret": "whsec_k7x9m2p4q8r1...",
#     "created_at": "2026-03-28T10:00:00Z"
#   }
# }

Webhook Payload

{
  "id": "5f1c2a7e-8b0d-4c3e-9a51-2d7f6b8e4c19",
  "object": "event",
  "event": "job.completed",
  "job_id": "0818df65-d092-5901-a506-3b2e64fb88ef",
  "type": "animation",
  "mascot_id": "c244e7b1-5655-5540-89a8-516b09bd4985",
  "asset_ids": {
    "image": "3c8ce568-35ce-59c2-b3f8-9fb7e4a16e52",
    "transparent_image": "477db326-3fb1-53aa-aa09-d8377daa1ffa",
    "video": "1b6520a1-a890-5a1c-ac87-15e9b14c4852",
    "webm": "3db2d3be-a85a-5e9e-85fc-02100b19d8cb",
    "hevc": "267afbae-2a4b-51c9-a172-47bb3017c32f"
  },
  "urls": {
    "image": "https://assets.masko.ai/c244e7b1-5655-5540-89a8-516b09bd4985/image.png",
    "transparent_image": "https://assets.masko.ai/c244e7b1-5655-5540-89a8-516b09bd4985/transparent.png",
    "video": "https://assets.masko.ai/c244e7b1-5655-5540-89a8-516b09bd4985/video.mp4",
    "webm": "https://assets.masko.ai/c244e7b1-5655-5540-89a8-516b09bd4985/video.webm",
    "hevc": "https://assets.masko.ai/c244e7b1-5655-5540-89a8-516b09bd4985/video_hevc.mp4"
  },
  "file_sizes": {
    "image": 412833,
    "transparent_image": 298120,
    "video": 2620007,
    "webm": 1370194,
    "hevc": 2397850
  },
  "timestamp": "2026-03-28T10:01:32Z"
}

id is the event's UUID. Retries resend the same id and body, so timestamp is when the event was created. Each request also carries these headers:

HeaderValue
Masko-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
Masko-Event-IdThe event ID, equal to id in the body
X-Masko-Signaturesha256= followed by the hex HMAC-SHA256 of the raw body
X-Masko-TimestampThe same unix seconds as t
X-Masko-EventThe event name, such as job.completed

Verify Signatures

Verify Masko-Signature, which signs the send time with the body so a captured request cannot be replayed. Compute the HMAC-SHA256 of <t>.<raw body> with your secret, compare it with v1 in constant time, and reject a t more than 5 minutes from your clock. Use the exact raw body, never re-serialized JSON.

import express from 'express';
import crypto from 'node:crypto';

const app = express();
const secret = process.env.MASKO_WEBHOOK_SECRET;
if (!secret) throw new Error('Set MASKO_WEBHOOK_SECRET');
const TOLERANCE_SECONDS = 5 * 60;

function verifyWebhook(rawBody, header) {
  const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header || '');
  if (!match) return false;
  const [, t, v1] = match;
  if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCE_SECONDS) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.`)
    .update(rawBody)
    .digest();
  const actual = Buffer.from(v1, 'hex');
  return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
}

// Register before any app.use(express.json()) middleware.
app.post('/api/masko-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!verifyWebhook(req.body, req.get('Masko-Signature'))) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body.toString('utf8'));
  // Store event.id with a unique constraint and skip events you have already seen.
  // Acknowledge only after your application has accepted the event durably.
  return res.sendStatus(200);
});

X-Masko-Signature signs only the body, so it does not protect against replays; prefer Masko-Signature.

Delivery and retries

A 2xx response within 10 seconds counts as delivered. Otherwise Masko retries the same event 6 more times, after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours (about 21 hours in total), stopping on success or when the webhook is deleted or disabled.

An event can arrive more than once, for example when your response was lost. Deduplicate by event id (also in Masko-Event-Id), acknowledge promptly after durable acceptance, and reconcile through the job endpoint if events stop.

Each failed attempt adds one to consecutive_failures and a success resets it to 0. At 100, Masko disables the webhook (active: false); delete it and create a new one to resume.

Only jobs started with an API key send webhooks, so poll jobs started elsewhere, such as in Studio. Payload fields depend on the workflow: rely on id, object, event and job_id, and fetch the job for details.

Manage Webhooks

curl https://api.masko.ai/v1/webhooks \
  -H "Authorization: Bearer masko_YOUR_API_KEY"

# Response:
# {
#   "data": [
#     {
#       "id": "wh_abc123",
#       "url": "https://your-app.com/api/masko-webhook",
#       "events": ["job.completed", "job.failed"],
#       "active": true,
#       "consecutive_failures": 0,
#       "created_at": "2026-03-28T10:00:00Z"
#     }
#   ]
# }
curl -X DELETE https://api.masko.ai/v1/webhooks/wh_abc123 \
  -H "Authorization: Bearer masko_YOUR_API_KEY"

# Response: 204 No Content