Templates

Fake Text

Send the conversation you want to see typed out and Vsub renders it as a chat on an iPhone, WhatsApp, Instagram, Tinder or Cantina screen, narrating each message in its character's voice over gameplay. Creation returns immediately, so poll the video or listen with webhooks.

POST/v1/fake-text-videos

Create a fake text video

Starts a new video, credits are charged the same way as videos created in the app.
Body
conversationsrequired
object[]
The chats the video plays through, one after another, the same as adding conversations in the app. Most videos need only one. See conversation fields. Up to 1,000 messages across all of them.
render
object
Export the mp4 as soon as the narration is ready, taking the same fields as the render endpoint, see render fields. Pass an empty object to accept the defaults. Left out, the video stays an editable project you can render later. Rendering charges export credits and requires a paid plan.
webhookUrl
string
HTTPS callback fired once the video reaches a terminal status. See Webhooks for details.
conversation fields
charactersrequired
object[]
Who is chatting, at least two. Each one gets a side of the screen and a voice, see character fields.
messagesrequired
object[]
The chat itself, in the order it plays. See message fields.
template
"iphone" | "iphone-dark" | "whatsapp" | "whatsapp-dark" | "instagram" | "instagram-dark" | "tinder" | "tinder-dark" | "cantina"
Which chat app the conversation is styled as.
default: iphone-dark
language
string
Language of the messages, as named in the language picker, such as Spanish or Japanese. Anything other than English narrates with the multilingual voice model.
default: English
autoSplit
boolean
Breaks each message into one message per sentence before narrating, so long messages arrive as a burst of short ones.
default: false
groupName
string
Title in the chat header. Shown once the conversation has more than two characters.
default: Group name
groupPhoto
string
Avatar shown in the chat header of a group conversation. Upload it with Files and pass back the file name it returns.
tinderMatch
string
The "you matched on" line above the chat. Tinder templates only.
useVoiceV2
boolean
Narrates with the newer ElevenLabs model. Turn it off to use the older one, which some voices read more evenly.
default: true
character fields
namerequired
string
Shown in the chat header and, in group chats, above the messages. Messages refer to a character by this name.
id
string
What messages refer to this character by, when the name is awkward to repeat or two characters share one. Defaults to the name.
side
"left" | "right"
Right is the phone owner, whose messages sit on the right in the sent bubble colour. Everyone else reads as the person being texted.
default: left
voiceId
string
ElevenLabs voice to narrate with. Defaults to the two voices the app starts you with, one per side. List the ids with Voices.
voiceProvider
"11" | "aws" | "openai"
Which text to speech service the voiceId belongs to. Voices lists ElevenLabs ids, the other two are for ids you already know.
default: 11
voiceStyle
object
Fine tunes the narration. Every field is a percentage, matching the voice sliders in the editor. Pass only the ones you want to change, see voiceStyle fields.
avatar
string
Profile picture. Templates fall back to their placeholder avatar. Upload it with Files and pass back the file name it returns.
color
string
Name colour in group chats.
default: #1677ff
userId
string
Handle under the name. Instagram templates only.
message fields
from
string
Which character sent it, matching their id or name. Required on text and image messages, ignored on hooks and comments since those have their own voice.
type
"text" | "image" | "hook" | "comment"
Text is a spoken chat bubble. Image attaches a picture. Hook is the app style card that opens a video, comment is the overlaid social comment.
default: text
text
string
What the bubble says and what the voice reads. Required on text messages, up to 400 characters.
emphasize
boolean
Reads the message as a shout, the narration is generated from the text uppercased with an exclamation mark.
default: false
image
string
Image, gif or video attached to the message. Required on image messages. Upload it with Files and pass back the file name it returns.
imageWidth
number
Pixel width of the file, as returned by the upload. Used to lay the attachment out, pass it with imageHeight or the bubble may be sized wrong.
imageHeight
number
Pixel height of the file.
imageDuration
number
How long a gif or video attachment stays on screen, in milliseconds. Still images hold for 1300.
hookType
"rizzApp" | "plugAi" | "custom"
Which hook card to draw. The first two mimic the apps of the same name, custom shows your own image.
default: custom
hook
object
What the hook says and how it looks, see hook fields. Hook messages only.
comment
object
Which voice reads the comment, see comment fields. Comment messages only.
hook fields
hook.text
string
Narrated over the hook.
default: Say something
hook.displayText
string
Written on the card. Defaults to hook.text, so set it only when the card should read differently to what is spoken.
hook.voiceId
string
Voice for the hook, independent of the characters.
default: pNInz6obpgDQGcFmaJgB
hook.voiceProvider
"11" | "aws" | "openai"
Service the hook voiceId belongs to.
default: 11
hook.voiceStyle
object
Same fields as voiceStyle on a character.
hook.image
string
Picture on the card. Custom hooks only, the other two draw their own. Upload it with Files and pass back the file name it returns.
hook.layout
object
Where the card sits and how it animates, see hook.layout fields. Pass only the ones you want to change.
hook.layout fields
hook.layout.paddingTop
number
Gap above the card in pixels, which is what moves it down the frame.
default: 300
hook.layout.paddingX
number
Gap either side of the card in pixels. Raise it to make the card narrower.
default: 100
hook.layout.radius
number
Corner rounding of the card in pixels.
default: 30
hook.layout.useShadow
boolean
Drops a shadow behind the card to lift it off the video.
default: true
hook.layout.shadowColor
string
Colour of that shadow, as any CSS colour.
default: rgba(0, 0, 0, 0.1)
hook.layout.shadowSize
number
How far the shadow spreads, in pixels.
default: 15
hook.layout.rizzTotalMessages
number, 0 to 100
How many chat lines the card types out before the conversation starts. rizzApp and plugAi hooks only.
default: 4
hook.layout.rizzDuration
number
How long that typing runs, in milliseconds. rizzApp and plugAi hooks only.
default: 2000
comment fields
comment.voiceId
string
Voice that reads the comment.
default: pNInz6obpgDQGcFmaJgB
comment.voiceProvider
"11" | "aws" | "openai"
Service the comment voiceId belongs to.
default: 11
comment.voiceStyle
object
Same fields as voiceStyle on a character.
voiceStyle fields
voiceStyle.stability
number, 0 to 100
Lower values let the voice vary more between takes, higher values keep it flat and predictable.
default: 38
voiceStyle.similarityBoost
number, 0 to 100
How closely the output sticks to the original voice.
default: 75
voiceStyle.style
number, 0 to 100
Style exaggeration. Higher values push the delivery harder, at some cost to stability.
default: 0
voiceStyle.speed
number, 70 to 120
Speaking rate as a percentage of the voice’s natural pace.
default: 100
render fields
render.format
"standard" | "greenscreen"
Standard renders the finished video. Green screen drops the background and renders the captions and overlays on a solid green fill, ready to key out in your own editor.
default: standard
render.frameRate
30 | 60
Frames per second. Rendering costs 30 credits per minute of video at 30fps and 60 credits per minute at 60fps.
default: 30
Request
curl -X POST "https://api.vsub.io/v1/fake-text-videos" \
  -H "Authorization: Bearer vp_a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90" \
  -H "Content-Type: application/json" \
  -d '{
    "conversations": [
      {
        "template": "iphone-dark",
        "characters": [
          { "name": "Mom", "side": "left" },
          { "name": "Me", "side": "right" }
        ],
        "messages": [
          { "from": "Mom", "text": "Where are you?" },
          { "from": "Me", "text": "Still at the office" },
          { "from": "Mom", "text": "It is 11pm", "emphasize": true }
        ]
      }
    ],
    "webhookUrl": "https://your-app.example.com/hooks/vsub-video"
  }'
Response 200
{
  "id": "9f0c1d5e-3a1b-4c2f-9d7e-8b1a2c3d4e5f",
  "status": "processing",
  "step": "create-vocal",
  "render": false,
  "createdAt": "2026-08-05T09:20:11.000Z",
  "editorUrl": "https://vsub.io/workspace/editor/9f0c1d5e-3a1b-4c2f-9d7e-8b1a2c3d4e5f"
}

Generation runs in the background. Follow the video with Video status or let a webhook tell you when it's done.

More examples

Each of these is the body of the same request, the url and headers don't change.

Group chat with an image

Three characters make it a group chat, so the header shows groupName and the photo, and each name is written in its own colour. The image message carries the key and size returned by Files, and autoSplit breaks Ana's long message into three that arrive one after another.

Body
{
  "conversations": [
    {
      "template": "whatsapp",
      "groupName": "Bali trip",
      "groupPhoto": "expire-30-user-image-2b7f9c14-8a3e-4d21-9f07-1c5b6a8e2d40.png",
      "autoSplit": true,
      "characters": [
        {
          "name": "Ana",
          "side": "left",
          "color": "#f759ab",
          "avatar": "expire-30-user-image-6c1a72f5-4b98-4e3a-8d16-9f2c0b7a5e31.png"
        },
        { "name": "Leo", "side": "left", "color": "#52c41a", "voiceId": "pNInz6obpgDQGcFmaJgB" },
        { "name": "Me", "side": "right" }
      ],
      "messages": [
        {
          "from": "Ana",
          "text": "I booked the flights. We land at 6am on the 4th. Do not plan anything for that morning."
        },
        { "from": "Leo", "text": "Look at the place I found" },
        {
          "from": "Leo",
          "type": "image",
          "image": "expire-30-user-image-91d0e4a7-2f65-4c88-b3a9-7e1d5c0f8b42.png",
          "imageWidth": 1080,
          "imageHeight": 1350
        },
        { "from": "Me", "text": "Book it", "emphasize": true }
      ]
    }
  ]
}

Opening on a hook, closing on a comment

The hook plays before the chat with its own voice, reading hook.text while the card shows the shorter displayText. The comment lands over the conversation at the end, also in its own voice. Neither takes a from. The characters here carry an id, which is handy when the on screen name is long or repeated.

Body
{
  "conversations": [
    {
      "template": "iphone-dark",
      "characters": [
        { "id": "her", "name": "Sofia", "side": "left" },
        {
          "id": "me",
          "name": "Me",
          "side": "right",
          "voiceId": "Er9R704Replxn1FtlRde",
          "voiceStyle": { "stability": 60, "speed": 105 }
        }
      ],
      "messages": [
        {
          "type": "hook",
          "hookType": "rizzApp",
          "hook": {
            "text": "She left me on read for three days, so I asked an app what to send",
            "displayText": "What do I send after three days?",
            "layout": { "rizzTotalMessages": 3, "rizzDuration": 1800 }
          }
        },
        { "from": "me", "text": "Still thinking about that taco place" },
        { "from": "her", "text": "You waited three days to say that" },
        {
          "type": "comment",
          "text": "he really used an app for this",
          "comment": { "voiceId": "aEO01A4wXwd1O8GPgGlF" }
        }
      ]
    }
  ]
}

Two conversations in one video

The conversations play back to back, each with its own app and cast, which is how you cut from the match to the fallout. Tinder templates take tinderMatch for the line above the chat, and Instagram templates show a character's userId as their handle.

Body
{
  "conversations": [
    {
      "template": "tinder",
      "tinderMatch": "You matched with Julia on 3 May",
      "characters": [
        { "name": "Julia", "side": "left" },
        { "name": "Me", "side": "right" }
      ],
      "messages": [
        { "from": "Julia", "text": "Hey, you look familiar" },
        { "from": "Me", "text": "We went to the same school" }
      ]
    },
    {
      "template": "instagram-dark",
      "characters": [
        { "name": "Julia", "userId": "@julia.k", "side": "left" },
        { "name": "Me", "userId": "@dan", "side": "right" }
      ],
      "messages": [
        { "from": "Julia", "text": "My sister just sent me your yearbook photo" },
        { "from": "Me", "text": "Please delete it", "emphasize": true }
      ]
    }
  ]
}

The gameplay behind the chat, the bubble styling and the sound effects come from your workspace defaults, the same ones the app starts a fake text video with. Anything the request doesn't cover, such as swapping the background or muting a single message, is still yours to change in the editor before rendering.