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
You send
POST /v1/batches with one row per customer
- 2
We reserve
1 credit per video, all or nothing
- 3
We render
in parallel, each a branded MP4
- 4
You receive
a signed webhook per video + per batch
Quick start
- Get a key. Sign in, open the dashboard and create an API key. Copy it: it is shown once.
- Add credits. New accounts get 3 free (watermarked) credits to test. Any credit pack removes the watermark and unlocks HD.
- Set a webhook (optional, recommended) in the dashboard, and verify its signature.
- 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.
Authentication
Send your key in the Authorization header (or X-API-Key). Keys look like mvk_live_1a2b3c4d_….
Authorization: Bearer mvk_live_1a2b3c4d_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/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
402and 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
refundedfield becomestrueand 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
renderQualityhigh/maxis 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
| Limit | Type | Description |
|---|---|---|
| Requests | per key | 120 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 batch | integer | 500. Send several batches for bigger lists (they queue fairly, oldest first). |
| Request body | bytes | 6 MB. Send image URLs, never file contents. |
| Rendering in parallel | platform | Videos start as capacity frees up; a 500-video batch typically completes in 30–60 minutes. |
| API keys | per account | 10 |
| metadata | object | Up to 20 keys, 2 KB, string / number / boolean values. Returned on the video and in webhooks. |
| Video length | seconds | 6–30 (default 15), 30 fps, H.264 MP4, in 9:16, 4:5, 1:1 or 16:9. |
| Videos per request | rows × sizes | At 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-1f3c3b0f9d21Templates
/v1/templatesThe template catalogueEach 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 yourdefaultsand 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.
| Parameters | Type | Description |
|---|---|---|
| templateId | string | Design to use, e.g. fashion_sale_01. Omitted: the category default. |
| templateUniqueId | string | Start from a gallery example (see Templates). |
| brandKitId | string | A saved brand kit's id. See Brand kit. |
| brandKit | object | A full brand kit inline: name, logos, colour codes, fonts, contacts. See Brand kit. |
| business.namerequired | string ≤70 | Shown on the video. Required unless the brand kit has a name. |
| business.logo | https URL | PNG / SVG / WebP, transparent background works best. |
| business.phone / whatsapp | string ≤24 | Contact line on the closing scene. |
| business.address | string ≤140 | |
| business.website / instagram / tagline | string | |
| promotion.title | string ≤60 | Headline, e.g. "Diwali Sale". |
| promotion.subtitle | string ≤70 | |
| promotion.offerText | string ≤90 | e.g. "Buy 2 get 1 free". |
| promotion.cta | string ≤28 | Call to action, e.g. "Order on WhatsApp". |
| promotion.discountPercent | number | Shows a discount badge. |
| promotion.validity | string ≤40 | e.g. "Till 31 Oct". |
| products[] | array ≤6 | name, price, salePrice, discountPercent, image (https), images[≤3], tag. |
| additionalImages[] | array ≤6 | Extra https image URLs. |
| language | string | e.g. en, hi. |
| currency | string | e.g. INR, USD. |
| duration | number | 6–30 seconds, default 15. |
| style | object | preset, accentColor. |
| music | object | src (https MP3), volume 0–1. |
| aspectRatio | string | Video size: "9:16" (default), "4:5", "1:1" or "16:9". See Video sizes. |
| aspectRatios | array | Several sizes at once, e.g. ["9:16", "1:1"]: one video per size, 1 credit each. |
| renderQuality | string | standard | high | max (high/max need a purchased pack). |
| externalId | string ≤128 | Your own id (customer, store, order). Returned everywhere. |
| metadata | object | Free-form data echoed back in the video and webhooks. |
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.
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.
| Parameters | Type | Description |
|---|---|---|
| name | string ≤70 | Brand name. Used as the business name when business.name is missing. |
| logo | https URL | Main logo: transparent PNG or SVG, 512–1024 px wide. |
| logoOnDark | https URL | Light version, used on dark backgrounds. |
| logoOnLight | https URL | Dark version, used on light backgrounds. |
| logoBackground | string | "auto" (default: white plate behind the main logo only), "none" (always transparent) or a colour for the plate. |
| logoPlacement | string | Corner logo: "top-left" (default), "top-right" or "none". |
| logoSize | string | "small", "medium" (default) or "large". |
| colors.primary | colour | Main brand colour: prices, CTA button, badges, highlights. |
| colors.secondary | colour | Second colour: glows and gradients. |
| colors.background | colour | Scene background (switches colorMode "auto" to "full"). |
| colors.surface | colour | Cards and panels. |
| colors.text | colour | Main text colour (used only if readable on the background). |
| colors.onPrimary | colour | Text on the primary colour, e.g. the CTA label. |
| colorMode | string | "auto" (default), "tinted" (template mood tinted to your hue), "full" (your background), "accent" (only the accent changes). |
| mode | string | "auto" (default), "dark" or "light": forces the mood in tinted mode. |
| fonts.heading | string | {url} | Titles, product names, prices. A built-in name, any Google Fonts family, or { "url": "https://…/Brand-Bold.woff2" }. |
| fonts.body | string | {url} | Labels, contacts, small text. Same formats as heading. |
| tagline / website / instagram | string | Defaults for the same business fields (≤60 / ≤60 / ≤40 chars). |
| phone / whatsapp / address | string | Contact 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.colorsmay 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
/v1/brand-kitsSave a kit (body: the brand kit above) · returns its id/v1/brand-kitsList your kits/v1/brand-kits/{kitId}One kit/v1/brand-kits/{kitId}Change only the fields you send (e.g. one colour)/v1/brand-kits/{kitId}Replace the whole kit/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.
| aspectRatio | Type | Description |
|---|---|---|
| "9:16" | 1080 × 1920 | Default. Reels, Stories, YouTube Shorts, WhatsApp Status. |
| "4:5" | 1080 × 1350 | Instagram and Facebook feed posts (takes the most room in a feed). |
| "1:1" | 1080 × 1080 | Square feed posts, WhatsApp, marketplace and ad placements. |
| "16:9" | 1920 × 1080 | YouTube, websites, TV and in-store screens. |
aspectRatiopicks 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.9x16and9/16are accepted. Anything else returns 400 before anything is charged. - In a batch,
aspectRatiosapplies to every row. A row may set its ownaspectRatiooraspectRatios, which wins for that row. - Every video in a response, list or webhook has an
aspectRatiofield, and the sameexternalIdfor 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.jsonVideos
/v1/videosCreate one video · 1 credit · HTTP 202Body: 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": []
}/v1/videos/{videoId}Status and result of one videoPoll 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.
/v1/videos?limit=25&cursor=…&status=doneYour videos, newest firstAll list endpoints return { data: [...], nextCursor, hasMore }. Pass nextCursor back as cursor for the next page (limit 1–100).
Batches (bulk)
/v1/batchesCreate up to 500 videos · 1 credit each · HTTP 202Each 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.
| Parameters | Type | Description |
|---|---|---|
| videos[]required | array | 1–500 rows, each a video input (+ externalId, metadata, brandKitId). |
| defaults | object | Video input shared by every row. |
| name | string ≤120 | Shown in the dashboard and webhooks. |
| templateId / templateUniqueId | string | Template for every row (a row may set its own templateId). |
| brandKitId | string | Saved brand kit for every row (a row may override it). |
| brandKit | object | Inline brand kit for every row. A row can send its own brandKit to override single fields (e.g. one colour). |
| aspectRatios | array | Sizes 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). |
| renderQuality | string | standard | high | max |
| webhookUrl | https URL | Overrides 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": []
}/v1/batches/{batchId}Progress counters/v1/batches/{batchId}/videos?status=failed&limit=100The batch's videos, in input order/v1/batchesYour batches, newest first/v1/batches/{batchId}/cancelCancel videos not started yetCancelling 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:
| Event | Type | Description |
|---|---|---|
| video.completed | video | The MP4 is ready: data.outputUrl. |
| video.failed | video | Render failed or timed out; data.error says why. The credit is already refunded. |
| batch.completed | batch | Every video of a batch has finished (done, failed or cancelled). Sent once per batch. |
| webhook.test | object | Sent 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
2xxwithin 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
httpson a public host. Redirects are not followed. - Rotating the secret takes effect immediately: deploy the new secret to your server right after.
/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-…"
}| HTTP | Type | Description |
|---|---|---|
| 400 | bad_request | Invalid JSON, a missing field (e.g. business.name), an invalid webhookUrl or cursor. |
| 401 | unauthorized | Missing, invalid or revoked API key. |
| 402 | insufficient_credits | Not enough credits for the whole request. The body has credits and required. Nothing was charged. |
| 404 | not_found | Unknown video, batch, template or brand kit (or one that belongs to another account). |
| 409 | conflict | An Idempotency-Key request is still in progress, or a limit (e.g. API keys) is reached. |
| 413 | too_large | More than 500 videos in one batch. |
| 422 | validation_failed | One or more batch rows are invalid. errors lists each index, externalId and message. Nothing was charged. |
| 429 | rate_limited | Too many requests. Wait for Retry-After seconds. |
| 500 / 503 | server_error | Temporary problem. Retry with backoff and the same Idempotency-Key. Quote requestId to support. |
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.jsonReady to build?
Create a key, send a test batch of 3 videos with your free credits, and watch it in the dashboard.