MsgVibe Business API · v1

Personalised promo videos, in bulk, from your own systems.

Send one request with hundreds of customers, stores or products. We render a branded 1080×1920 MP4 for each, notify your server when each one is ready, and only charge for videos that succeed.

Up to 500 videos per request

Queued and rendered in parallel, oldest first.

Pay only for success

Failed or cancelled videos are refunded automatically.

Secure by default

Hashed keys, signed webhooks, idempotent retries.

Introduction

The API is REST over HTTPS with JSON bodies. Every response has "status": "success" or "status": "error". Video creation is asynchronous: you get an id back immediately (HTTP 202), the video renders in the background (usually 40–90 seconds, longer when a large batch is queued), and you learn the result from a webhook or by polling.

Base URL

https://api.msgvibe.com/v1
  1. 1

    You send

    POST /v1/batches with one row per customer

  2. 2

    We reserve

    1 credit per video, all or nothing

  3. 3

    We render

    in parallel, each a branded MP4

  4. 4

    You receive

    a signed webhook per video + per batch

Quick start

  1. Get a key. Sign in, open the dashboard and create an API key. Copy it: it is shown once.
  2. Add credits. New accounts get 3 free (watermarked) credits to test. Any credit pack removes the watermark and unlocks HD.
  3. Set a webhook (optional, recommended) in the dashboard, and verify its signature.
  4. Create a video and wait for video.completed:
curl -X POST https://api.msgvibe.com/v1/videos \
  -H "Authorization: Bearer $MSGVIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "fashion_sale_01",
    "business": { "name": "Sharma Garments", "phone": "+91 987xx xxxxx" },
    "promotion": { "title": "Diwali Sale", "cta": "Visit today" }
  }'

Try it live

Send real requests to https://api.msgvibe.com/v1 from this page. Paste your API key, pick a request, edit the JSON and press Send. Start with GET /account to check your key, then create a video and check its status. Every request here can also be copied as cURL.

Paste a full API key (mvk_live_…) to send.

Authentication

Send your key in the Authorization header (or X-API-Key). Keys look like mvk_live_1a2b3c4d_….

Authorization: Bearer mvk_live_1a2b3c4d_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Keep keys on your server. Anyone with a key can spend your credits. Never put one in a mobile app, browser code or a public repository. Browsers can call the API only from www.msgvibe.com (for the console below); other websites are blocked. If a key leaks, revoke it in the dashboard: it stops working immediately. Use one key per system so you can rotate them independently.
GET/v1/accountBalance, plan and your limits
{
  "status": "success",
  "account": {
    "credits": 247, "plan": "paid", "renderCost": 1, "watermark": false,
    "limits": { "maxVideosPerBatch": 500, "requestsPerMinute": 120, "maxApiKeys": 10 }
  }
}

Credits & billing

  • 1 credit = 1 video, any template, up to 30 seconds.
  • Credits are reserved when the request is accepted, for the whole batch at once. If your balance can't cover every video, the request fails with 402 and nothing is charged: a batch never half-starts.
  • A video that fails to render, times out or is cancelled before it starts is refunded automatically, exactly once. Its refunded field becomes true and a refund line appears in your billing history.
  • Plan rules are applied by the server: on the free plan every video carries the MsgVibe watermark and renders in standard quality. After any credit purchase, videos have no watermark and renderQuality high / max is honoured. A request can't override this.
  • Buy credits on the billing page (UPI/cards via Razorpay, or PayPal). Every movement is in the ledger.

Rate limits & limits

LimitTypeDescription
Requestsper key120 per minute. Every response has X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds). Over the limit: 429 with Retry-After.
Videos per batchinteger500. Send several batches for bigger lists (they queue fairly, oldest first).
Request bodybytes6 MB. Send image URLs, never file contents.
Rendering in parallelplatformVideos start as capacity frees up; a 500-video batch typically completes in 30–60 minutes.
API keysper account10
metadataobjectUp to 20 keys, 2 KB, string / number / boolean values. Returned on the video and in webhooks.
Video lengthseconds6–30 (default 15), 30 fps, H.264 MP4, in 9:16, 4:5, 1:1 or 16:9.
Videos per requestrows × sizesAt most 500 per batch, counting every size (250 rows × 2 sizes = 500).

Idempotency

Networks fail. To retry a POST /v1/videos or POST /v1/batches safely, send an Idempotency-Key header (8–100 characters, e.g. a UUID or your own job id). For 24 hours, a repeat of the same request returns the original response (with header Idempotent-Replayed: true) instead of creating and charging again. Reusing a key with a different body returns 422; a retry while the first is still running returns 409.

Idempotency-Key: 6f1c2b8e-5f3a-4a7e-9a51-1f3c3b0f9d21

Templates

GET/v1/templatesThe template catalogue

Each template has a templateId (the design, e.g. fashion_sale_01) and a uniqueId (a ready-made example in the gallery). Pass either:

  • templateId: the design only; you supply all the content.
  • templateUniqueId: starts from the gallery example's full content (texts, demo products, music), which your defaults and each row then override.

Browse them visually in the template gallery; a template's id is in its studio URL.

{
  "status": "success",
  "count": 50,
  "data": [
    { "uniqueId": "01_fashion_sale", "templateId": "fashion_sale_01", "name": "Fashion sale",
      "categoryId": "fashion", "thumbnailUrl": "https://…", "previewVideoUrl": "https://…" }
  ]
}

Video input

The same fields work for a single video, for a batch's defaults and for each batch row. Unknown fields are ignored; text is trimmed to the lengths shown so it always fits the design.

ParametersTypeDescription
templateIdstringDesign to use, e.g. fashion_sale_01. Omitted: the category default.
templateUniqueIdstringStart from a gallery example (see Templates).
brandKitIdstringA saved brand kit's id. See Brand kit.
brandKitobjectA full brand kit inline: name, logos, colour codes, fonts, contacts. See Brand kit.
business.namerequiredstring ≤70Shown on the video. Required unless the brand kit has a name.
business.logohttps URLPNG / SVG / WebP, transparent background works best.
business.phone / whatsappstring ≤24Contact line on the closing scene.
business.addressstring ≤140
business.website / instagram / taglinestring
promotion.titlestring ≤60Headline, e.g. "Diwali Sale".
promotion.subtitlestring ≤70
promotion.offerTextstring ≤90e.g. "Buy 2 get 1 free".
promotion.ctastring ≤28Call to action, e.g. "Order on WhatsApp".
promotion.discountPercentnumberShows a discount badge.
promotion.validitystring ≤40e.g. "Till 31 Oct".
products[]array ≤6name, price, salePrice, discountPercent, image (https), images[≤3], tag.
additionalImages[]array ≤6Extra https image URLs.
languagestringe.g. en, hi.
currencystringe.g. INR, USD.
durationnumber6–30 seconds, default 15.
styleobjectpreset, accentColor.
musicobjectsrc (https MP3), volume 0–1.
aspectRatiostringVideo size: "9:16" (default), "4:5", "1:1" or "16:9". See Video sizes.
aspectRatiosarraySeveral sizes at once, e.g. ["9:16", "1:1"]: one video per size, 1 credit each.
renderQualitystringstandard | high | max (high/max need a purchased pack).
externalIdstring ≤128Your own id (customer, store, order). Returned everywhere.
metadataobjectFree-form data echoed back in the video and webhooks.
Images and logos are downloaded by our renderer, so they must be public https URLs. If an image fails to load, the video still renders with a tasteful fallback.

Brand kit

A brand kit puts a company's identity on any template: name, logo, colour codes, fonts and contact details. The template keeps its layout and motion; the brand decides how it looks. There are two ways to send one, and you can combine them:

1. Inline brandKit object

Send the whole kit in the request. Best when every customer has their own brand (agencies, franchise data from your CRM). Nothing to save first.

2. Saved kit: brandKitId

Save it once (on the Brand kits page or with POST /v1/brand-kits), then send only its id. Max 20 kits per account.

Which value wins: a field in the row's brandKit > the batch's brandKit > the saved kit from brandKitId. Only the fields you send override; the rest are kept. business.* (name, phone, address…) always beats the kit's contact defaults, so one kit serves many branches.

Every brand kit field

All fields are optional, but a kit needs at least one of name, a logo, a colour or a font. Even { "colors": { "primary": "#E50914" } } alone brands a video. Values that aren't valid are ignored and listed in the response's warnings; they never fail the request.

ParametersTypeDescription
namestring ≤70Brand name. Used as the business name when business.name is missing.
logohttps URLMain logo: transparent PNG or SVG, 512–1024 px wide.
logoOnDarkhttps URLLight version, used on dark backgrounds.
logoOnLighthttps URLDark version, used on light backgrounds.
logoBackgroundstring"auto" (default: white plate behind the main logo only), "none" (always transparent) or a colour for the plate.
logoPlacementstringCorner logo: "top-left" (default), "top-right" or "none".
logoSizestring"small", "medium" (default) or "large".
colors.primarycolourMain brand colour: prices, CTA button, badges, highlights.
colors.secondarycolourSecond colour: glows and gradients.
colors.backgroundcolourScene background (switches colorMode "auto" to "full").
colors.surfacecolourCards and panels.
colors.textcolourMain text colour (used only if readable on the background).
colors.onPrimarycolourText on the primary colour, e.g. the CTA label.
colorModestring"auto" (default), "tinted" (template mood tinted to your hue), "full" (your background), "accent" (only the accent changes).
modestring"auto" (default), "dark" or "light": forces the mood in tinted mode.
fonts.headingstring | {url}Titles, product names, prices. A built-in name, any Google Fonts family, or { "url": "https://…/Brand-Bold.woff2" }.
fonts.bodystring | {url}Labels, contacts, small text. Same formats as heading.
tagline / website / instagramstringDefaults for the same business fields (≤60 / ≤60 / ≤40 chars).
phone / whatsapp / addressstringContact defaults (≤24 / ≤24 / ≤140 chars).
  • Colour codes accept #E50914, #E09, E50914, rgb(229, 9, 20), hsl(357, 92%, 47%) or common names (red, navy, gold…). They are returned as #RRGGBB. colors may also be an array: ["#E50914", "#FFB800"] = primary, secondary (, background).
  • Readability is automatic: if a brand colour would be hard to read on video, the renderer adjusts its lightness just enough to pass contrast (text 7:1, brand colour 3:1, CTA text 4.5:1).
  • Built-in fonts (no download): anton, bebas, montserrat, playfair, archivo, dm serif, space grotesk, caveat, oswald, righteous, titan, rubik mono, poppins.
  • Never fails on assets: a broken or slow logo falls back to the next variant or a monogram; a font that doesn't load within 8 s falls back to the template font.

Complete brand kit

{
  "name": "Acme Fashion",
  "logo": "https://www.msgvibe.com/brand/msgvibe-logo.png",
  "logoOnDark": "https://www.msgvibe.com/brand/msgvibe-logo.png",
  "logoOnLight": "https://www.msgvibe.com/brand/msgvibe-logo.png",
  "logoBackground": "auto",
  "logoPlacement": "top-left",
  "logoSize": "medium",
  "colors": {
    "primary": "#E50914",
    "secondary": "#FFB800",
    "background": "#141414",
    "surface": "#1F1F1F",
    "text": "#FFFFFF",
    "onPrimary": "#FFFFFF"
  },
  "colorMode": "auto",
  "mode": "auto",
  "fonts": {
    "heading": "Poppins",
    "body": "Montserrat"
  },
  "tagline": "Wear the bold",
  "website": "acmefashion.in",
  "instagram": "@acmefashion",
  "phone": "+91 987xx xxxxx",
  "whatsapp": "+91 987xx xxxxx",
  "address": "MG Road, Pune"
}

Save a kit once, reuse it by id

POST/v1/brand-kitsSave a kit (body: the brand kit above) · returns its id
GET/v1/brand-kitsList your kits
GET/v1/brand-kits/{kitId}One kit
PATCH/v1/brand-kits/{kitId}Change only the fields you send (e.g. one colour)
PUT/v1/brand-kits/{kitId}Replace the whole kit
DELETE/v1/brand-kits/{kitId}Delete it
{
  "status": "success",
  "brandKit": { "id": "acme-fashion-1a2b3c4d", "name": "Acme Fashion", "colors": { "primary": "#E50914", … }, … },
  "warnings": []
}

Kits saved on the website's Brand kits page work the same way: their id is shown there with a copy button.

Video sizes (aspect ratios)

Every template renders in four sizes. You don't need separate templates: the layout reflows to the shape you ask for, with your brand kit, text and products, and nothing is cropped away.

aspectRatioTypeDescription
"9:16"1080 × 1920Default. Reels, Stories, YouTube Shorts, WhatsApp Status.
"4:5"1080 × 1350Instagram and Facebook feed posts (takes the most room in a feed).
"1:1"1080 × 1080Square feed posts, WhatsApp, marketplace and ad placements.
"16:9"1920 × 1080YouTube, websites, TV and in-store screens.
  • aspectRatio picks one size. aspectRatios (an array) makes one video per size from the same input, for example the same offer as a Reel and as a square post.
  • Each size is a separate video and costs 1 credit. If one size fails, only that one is refunded.
  • Aliases work too: portrait / reel = 9:16, feed = 4:5, square = 1:1, landscape / youtube = 16:9. 9x16 and 9/16 are accepted. Anything else returns 400 before anything is charged.
  • In a batch, aspectRatios applies to every row. A row may set its own aspectRatio or aspectRatios, which wins for that row.
  • Every video in a response, list or webhook has an aspectRatio field, and the same externalId for all sizes of one row.

One request, all four sizes

{
  "templateId": "fashion_sale_01",
  "aspectRatios": [
    "9:16",
    "4:5",
    "1:1",
    "16:9"
  ],
  "brandKit": {
    "name": "Acme Fashion",
    "logo": "https://www.msgvibe.com/brand/msgvibe-logo.png",
    "colors": {
      "primary": "#E50914",
      "secondary": "#FFB800"
    }
  },
  "business": {
    "name": "Acme Fashion - Pune",
    "phone": "+91 987xx xxxxx"
  },
  "promotion": {
    "title": "Diwali Sale",
    "subtitle": "Flat 40% off",
    "cta": "Visit today"
  },
  "products": [
    {
      "name": "Premium Shirt",
      "price": 1499,
      "salePrice": 999,
      "image": "https://images.unsplash.com/photo-1596755094514-f87e34085b2c?w=1200&q=80&auto=format&fit=crop"
    }
  ]
}

Full payload examples

Copy one, put your own values in, and send it (or paste it into Try it live). Every image URL in these examples really loads.

One video, every field

{
  "templateId": "fashion_sale_01",
  "aspectRatio": "9:16",
  "externalId": "store-001",
  "metadata": {
    "crmId": "C-1042",
    "campaign": "diwali-2026"
  },
  "renderQuality": "standard",
  "brandKit": {
    "name": "Acme Fashion",
    "logo": "https://www.msgvibe.com/brand/msgvibe-logo.png",
    "logoOnDark": "https://www.msgvibe.com/brand/msgvibe-logo.png",
    "logoOnLight": "https://www.msgvibe.com/brand/msgvibe-logo.png",
    "logoBackground": "auto",
    "logoPlacement": "top-left",
    "logoSize": "medium",
    "colors": {
      "primary": "#E50914",
      "secondary": "#FFB800",
      "background": "#141414",
      "surface": "#1F1F1F",
      "text": "#FFFFFF",
      "onPrimary": "#FFFFFF"
    },
    "colorMode": "auto",
    "mode": "auto",
    "fonts": {
      "heading": "Poppins",
      "body": "Montserrat"
    },
    "tagline": "Wear the bold",
    "website": "acmefashion.in",
    "instagram": "@acmefashion",
    "phone": "+91 987xx xxxxx",
    "whatsapp": "+91 987xx xxxxx",
    "address": "MG Road, Pune"
  },
  "business": {
    "name": "Acme Fashion - Pune",
    "phone": "+91 987xx xxxxx",
    "whatsapp": "+91 987xx xxxxx",
    "address": "MG Road, Pune",
    "website": "acmefashion.in",
    "instagram": "@acmefashion",
    "tagline": "Wear the bold"
  },
  "promotion": {
    "title": "Diwali Sale",
    "subtitle": "Flat 40% off on festive wear",
    "offerText": "Buy 2, get 1 free",
    "cta": "Visit today",
    "discountPercent": 40,
    "validity": "Till 31 Oct"
  },
  "products": [
    {
      "name": "Premium Shirt",
      "price": 1499,
      "salePrice": 999,
      "image": "https://images.unsplash.com/photo-1596755094514-f87e34085b2c?w=1200&q=80&auto=format&fit=crop",
      "tag": "Bestseller"
    },
    {
      "name": "Denim Jeans",
      "price": 2499,
      "salePrice": 1799,
      "image": "https://images.unsplash.com/photo-1542272604-787c3835535d?w=1200&q=80&auto=format&fit=crop"
    },
    {
      "name": "Bomber Jacket",
      "price": 3999,
      "salePrice": 2999,
      "image": "https://images.unsplash.com/photo-1591047139829-d91aecb6caea?w=1200&q=80&auto=format&fit=crop",
      "tag": "New"
    }
  ],
  "language": "en",
  "currency": "INR",
  "duration": 15
}

Batch: one brand for all rows, two sizes each

2 rows × 2 sizes = 4 videos. Row 2 overrides one brand colour and the subtitle; everything else comes from brandKit and defaults.

{
  "name": "Diwali - Acme stores",
  "templateId": "fashion_sale_01",
  "aspectRatios": [
    "9:16",
    "1:1"
  ],
  "renderQuality": "standard",
  "brandKit": {
    "name": "Acme Fashion",
    "logo": "https://www.msgvibe.com/brand/msgvibe-logo.png",
    "logoOnDark": "https://www.msgvibe.com/brand/msgvibe-logo.png",
    "logoOnLight": "https://www.msgvibe.com/brand/msgvibe-logo.png",
    "logoBackground": "auto",
    "logoPlacement": "top-left",
    "logoSize": "medium",
    "colors": {
      "primary": "#E50914",
      "secondary": "#FFB800",
      "background": "#141414",
      "surface": "#1F1F1F",
      "text": "#FFFFFF",
      "onPrimary": "#FFFFFF"
    },
    "colorMode": "auto",
    "mode": "auto",
    "fonts": {
      "heading": "Poppins",
      "body": "Montserrat"
    },
    "tagline": "Wear the bold",
    "website": "acmefashion.in",
    "instagram": "@acmefashion",
    "phone": "+91 987xx xxxxx",
    "whatsapp": "+91 987xx xxxxx",
    "address": "MG Road, Pune"
  },
  "defaults": {
    "promotion": {
      "title": "Diwali Sale",
      "subtitle": "Flat 40% off",
      "cta": "Visit today",
      "validity": "Till 31 Oct"
    },
    "products": [
      {
        "name": "Premium Shirt",
        "price": 1499,
        "salePrice": 999,
        "image": "https://images.unsplash.com/photo-1596755094514-f87e34085b2c?w=1200&q=80&auto=format&fit=crop",
        "tag": "Bestseller"
      },
      {
        "name": "Denim Jeans",
        "price": 2499,
        "salePrice": 1799,
        "image": "https://images.unsplash.com/photo-1542272604-787c3835535d?w=1200&q=80&auto=format&fit=crop"
      },
      {
        "name": "Bomber Jacket",
        "price": 3999,
        "salePrice": 2999,
        "image": "https://images.unsplash.com/photo-1591047139829-d91aecb6caea?w=1200&q=80&auto=format&fit=crop",
        "tag": "New"
      }
    ],
    "currency": "INR"
  },
  "videos": [
    {
      "externalId": "store-pune",
      "business": {
        "name": "Acme Fashion - Pune",
        "phone": "+91 987xx xxxxx",
        "address": "MG Road, Pune"
      }
    },
    {
      "externalId": "store-mumbai",
      "business": {
        "name": "Acme Fashion - Mumbai",
        "phone": "+91 912xx xxxxx",
        "address": "Linking Road, Mumbai"
      },
      "promotion": {
        "subtitle": "Extra 10% for members"
      },
      "brandKit": {
        "colors": {
          "primary": "#0057FF"
        }
      }
    }
  ]
}

The smallest valid request

{
  "templateId": "fashion_sale_01",
  "business": {
    "name": "Acme Fashion"
  }
}

Send it

# save the JSON above as payload.json, then:
curl -X POST https://api.msgvibe.com/v1/videos \
  -H "Authorization: Bearer $MSGVIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data @payload.json

Videos

POST/v1/videosCreate one video · 1 credit · HTTP 202

Body: the video input, plus optional webhookUrl to override your account webhook for this video.

curl -X POST https://api.msgvibe.com/v1/videos \
  -H "Authorization: Bearer $MSGVIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-video" \
  -d '{
    "templateId": "fashion_sale_01",
    "externalId": "customer-1042",
    "metadata": { "crmId": "C-1042", "campaign": "diwali" },
    "business": {
      "name": "Sharma Garments",
      "logo": "https://cdn.example.com/logos/sharma.png",
      "phone": "+91 987xx xxxxx",
      "address": "MG Road, Pune"
    },
    "promotion": { "title": "Diwali Sale", "subtitle": "Flat 40% off", "cta": "Visit today" },
    "products": [
      { "name": "Silk Kurta", "price": 2499, "salePrice": 1499, "image": "https://cdn.example.com/p/kurta.jpg" }
    ]
  }'
{
  "status": "success",
  "video": { "id": "vid_1791306844787a1b2c3d4_0", "externalId": "customer-1042", "status": "queued", "creditsCharged": 1, … },
  "credits": 246,
  "warnings": []
}
GET/v1/videos/{videoId}Status and result of one video

Poll every 10–15 seconds if you don't use webhooks. When status is done, outputUrl is the MP4. Download and store it on your side: links are meant for delivery, not permanent hosting.

GET/v1/videos?limit=25&cursor=…&status=doneYour videos, newest first

All list endpoints return { data: [...], nextCursor, hasMore }. Pass nextCursor back as cursor for the next page (limit 1–100).

Batches (bulk)

POST/v1/batchesCreate up to 500 videos · 1 credit each · HTTP 202

Each video is built as template example → defaults → row, merged field by field (objects merge, arrays and values replace). Every row is validated before anything is charged: if any row is invalid you get 422 with the list of problems and no credits move.

ParametersTypeDescription
videos[]requiredarray1–500 rows, each a video input (+ externalId, metadata, brandKitId).
defaultsobjectVideo input shared by every row.
namestring ≤120Shown in the dashboard and webhooks.
templateId / templateUniqueIdstringTemplate for every row (a row may set its own templateId).
brandKitIdstringSaved brand kit for every row (a row may override it).
brandKitobjectInline brand kit for every row. A row can send its own brandKit to override single fields (e.g. one colour).
aspectRatiosarraySizes for every row, e.g. ["9:16", "1:1", "16:9"]. Each row × each size is one video (1 credit). A row can set its own aspectRatio(s).
renderQualitystringstandard | high | max
webhookUrlhttps URLOverrides the account webhook for this batch.
curl -X POST https://api.msgvibe.com/v1/batches \
  -H "Authorization: Bearer $MSGVIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: diwali-2026-stores" \
  -d '{
    "name": "Diwali - all franchise stores",
    "templateId": "fashion_sale_01",
    "brandKitId": "acme-retail",
    "renderQuality": "high",
    "webhookUrl": "https://api.example.com/webhooks/msgvibe",
    "defaults": {
      "promotion": { "title": "Diwali Sale", "subtitle": "Up to 50% off", "cta": "Shop now" },
      "language": "en"
    },
    "videos": [
      { "externalId": "store-001", "business": { "name": "Acme Pune",   "phone": "+91 987xx xxxxx" } },
      { "externalId": "store-002", "business": { "name": "Acme Mumbai", "phone": "+91 912xx xxxxx" },
        "promotion": { "subtitle": "Extra 10% for members" } }
    ]
  }'
{
  "status": "success",
  "batch": { "id": "bat_1791306844787a1b2c3d4", "status": "queued", "total": 2, "creditsReserved": 2, … },
  "videos": [
    { "id": "vid_1791306844787a1b2c3d4_0", "externalId": "store-001", "status": "queued" },
    { "id": "vid_1791306844787a1b2c3d4_1", "externalId": "store-002", "status": "queued" }
  ],
  "credits": 245,
  "warnings": []
}
GET/v1/batches/{batchId}Progress counters
GET/v1/batches/{batchId}/videos?status=failed&limit=100The batch's videos, in input order
GET/v1/batchesYour batches, newest first
POST/v1/batches/{batchId}/cancelCancel videos not started yet

Cancelling refunds every video still queued; videos already rendering finish normally. The response has the number cancelled and your new balance. Cancelled videos send no per-video webhook; batch.completed is sent once the batch has nothing left to render.

Objects & statuses

queued→rendering→doneor failed (refunded) or cancelled (refunded)

A batch is queued until its first video starts, processing while any video is queued or rendering, and completed when every video is done, failed or cancelled.

{
  "id": "vid_1791306844787a1b2c3d4_0",
  "batchId": "bat_1791306844787a1b2c3d4",
  "externalId": "store-001",
  "status": "done",
  "templateId": "fashion_sale_01",
  "title": "Diwali Sale",
  "renderQuality": "high",
  "aspectRatio": "9:16",
  "watermark": false,
  "durationSeconds": 15,
  "outputUrl": "https://…/renders/…/out.mp4",
  "outputSizeBytes": 3145728,
  "renderSeconds": 48.2,
  "creditsCharged": 1,
  "refunded": false,
  "metadata": { "crmId": "C-1042" },
  "createdAt": "2026-10-06T10:15:02.112Z",
  "startedAt": "2026-10-06T10:15:04.020Z",
  "finishedAt": "2026-10-06T10:15:53.517Z"
}
{
  "id": "bat_1791306844787a1b2c3d4",
  "name": "Diwali - all franchise stores",
  "status": "processing",
  "total": 250,
  "counts": { "queued": 180, "rendering": 15, "done": 54, "failed": 1, "cancelled": 0 },
  "progress": 22.0,
  "creditsReserved": 250,
  "creditsRefunded": 1,
  "creditsCharged": 249,
  "templateId": "fashion_sale_01",
  "renderQuality": "high",
  "webhookUrl": "https://api.example.com/webhooks/msgvibe",
  "createdAt": "2026-10-06T10:15:02.112Z"
}

Webhooks

Set an endpoint in the dashboard (or webhookUrl per request). We POST JSON when something happens:

EventTypeDescription
video.completedvideoThe MP4 is ready: data.outputUrl.
video.failedvideoRender failed or timed out; data.error says why. The credit is already refunded.
batch.completedbatchEvery video of a batch has finished (done, failed or cancelled). Sent once per batch.
webhook.testobjectSent by "Send test" in the dashboard or POST /v1/webhooks/test.
POST /webhooks/msgvibe HTTP/1.1
Content-Type: application/json
User-Agent: MsgVibe-Webhooks/1.0
X-MsgVibe-Event: video.completed
X-MsgVibe-Delivery: 3f9a0c6e1b2d4c8f9e7a6b5c4d3e2f10
X-MsgVibe-Signature: t=1791306953,v1=8d5e…a41c

{"event":"video.completed","deliveryId":"3f9a0c6e…","createdAt":1791306953,
 "data":{"id":"vid_1791306844787a1b2c3d4_0","externalId":"store-001","status":"done",
         "outputUrl":"https://…/out.mp4","metadata":{"crmId":"C-1042"}, …}}

Verify every request

Webhook URLs are public, so anyone could post to yours. Check the signature with your signing secret (whsec_…, dashboard → Webhook → Reveal) before trusting an event:

# Signature header:  X-MsgVibe-Signature: t=1791306900,v1=5f2c…e9
# signed payload  =  "<t>.<raw request body>"
# expected        =  hex(HMAC-SHA256(webhook_secret, signed_payload))
# Compare in constant time and reject timestamps older than 5 minutes.

Delivery rules

  • Respond with any 2xx within 5 seconds; do slow work after responding.
  • Failed deliveries are retried after 30 s, 2 min, 10 min and 15 min, then dropped. The result is always available from GET /v1/videos/{id}.
  • An event may arrive more than once or out of order: de-duplicate on X-MsgVibe-Delivery / the video id and status.
  • Endpoints must be https on a public host. Redirects are not followed.
  • Rotating the secret takes effect immediately: deploy the new secret to your server right after.
POST/v1/webhooks/testSend a signed test event (optional body: { "url": "https://…" })

Errors

Errors use normal HTTP status codes and a JSON body. requestId identifies the request in our logs.

{
  "status": "error",
  "error": "Not enough credits: this request needs 250, your balance is 120.",
  "code": "insufficient_credits",
  "credits": 120,
  "required": 250,
  "requestId": "c0a8f1e2-…"
}
HTTPTypeDescription
400bad_requestInvalid JSON, a missing field (e.g. business.name), an invalid webhookUrl or cursor.
401unauthorizedMissing, invalid or revoked API key.
402insufficient_creditsNot enough credits for the whole request. The body has credits and required. Nothing was charged.
404not_foundUnknown video, batch, template or brand kit (or one that belongs to another account).
409conflictAn Idempotency-Key request is still in progress, or a limit (e.g. API keys) is reached.
413too_largeMore than 500 videos in one batch.
422validation_failedOne or more batch rows are invalid. errors lists each index, externalId and message. Nothing was charged.
429rate_limitedToo many requests. Wait for Retry-After seconds.
500 / 503server_errorTemporary problem. Retry with backoff and the same Idempotency-Key. Quote requestId to support.
Retry only 429, 500 and 503, with exponential backoff and the same Idempotency-Key. Other 4xx errors need a change to the request.

Security checklist

  • Keys stay server-side

    Environment variables or a secret manager. Never in apps, web pages or git.

  • One key per system

    Name keys after where they run; revoke one without touching the others.

  • Verify webhook signatures

    Constant-time comparison, reject timestamps older than 5 minutes.

  • Use Idempotency-Key

    Retries can never double-charge.

  • Store results yourself

    Download finished MP4s into your own storage.

  • Don't send secrets in metadata

    It is echoed back in responses and webhooks.

On our side: keys are random 256-bit secrets stored only as SHA-256 hashes; every query is scoped to the key's account; credits move in atomic database transactions; webhook delivery refuses private and internal network addresses; browsers may call the API only from www.msgvibe.com.

Full example: CSV → videos

Turn a spreadsheet of stores into one video each, split into batches of 500, then collect the results.

# customers.csv → JSON with any tool you like (jq, Python, Node), then:
curl -X POST https://api.msgvibe.com/v1/batches \
  -H "Authorization: Bearer $MSGVIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: diwali-2026-run-1" \
  --data @batch.json

Ready to build?

Create a key, send a test batch of 3 videos with your free credits, and watch it in the dashboard.