Reference
API reference
All paths are relative to https://app.personade.com/api/v1. Every request needs an Authorization: Bearer header. Every response is JSON.
Endpoints marked writes need a key with writing switched on. A read-only key gets 403 forbidden.
GET/templates
Your recordings, and whether each one can be personalized from right now. When usable is false, blockedReason says what a person has to do about it: the voice is still cloning, no video was uploaded, or the recording has no name in it to swap.
Each template
id | string | Pass this as templateId when you create a campaign. |
name | string | What the recording is called in Personade. |
usable | boolean | Whether it can be personalized from right now. Check this before using it. |
blockedReason | string? | Present only when usable is false. A sentence for a person, not a code. |
createdAt | string | ISO 8601, UTC. |
{
"templates": [
{ "id": "9f1c...", "name": "Founder intro, 40s",
"usable": true, "createdAt": "2026-08-02T09:14:22.000Z" },
{ "id": "5c02...", "name": "Q3 re-record",
"usable": false, "blockedReason": "The voice is still being prepared. Try again shortly.",
"createdAt": "2026-08-11T16:02:10.000Z" }
]
}Capped at 50, newest last. A workspace has a handful of recordings, not thousands.
GET/campaigns
Your campaigns, newest first. Takes an optional ?limit= between 1 and 100, default 25.
Each campaign
id | string | Use this in every other campaign endpoint. |
name | string | What you called it. |
status | string | One of eight values. See Polling and delivery. |
leadCount | number | Everyone in it, whatever state they are in. |
waiting | number | Never attempted. What a generate would charge for. |
complete | number | Videos finished. |
failed | number | Videos that failed. Their credits come back. |
nextStep | string | A sentence for a person. Wording changes; never branch on it. |
createdAt | string | ISO 8601, UTC. |
{
"campaigns": [
{ "id": "3b7e...", "name": "Series A ops leads", "status": "qa_ready",
"leadCount": 42, "waiting": 39, "complete": 3, "failed": 0,
"nextStep": "3 previews ready to watch, 39 people waiting on an approval.",
"createdAt": "2026-08-13T10:05:44.000Z" }
]
}nextStep exists because a script that stops halfway leaves a person with no record of where they were. It is written for a human and its wording will change.
POST/campaigns
Start a campaign on a recording that already exists. Writes.
Body
templateIdrequired | string | The recording to build on, from GET /templates. |
namerequired | string | What to call it, for your own reference. Up to 80 characters. |
goal | string | Optional note on who this targets. Internal only, never shown to a recipient. |
Response
id | string | The new campaign. Keep it. |
name | string | As you sent it, trimmed to 80 characters. |
templateId | string | The recording it was built on. |
status | string | Always draft. It has nobody in it yet. |
{ "id": "3b7e...", "name": "Series A ops leads",
"templateId": "9f1c...", "status": "draft" }POST/campaigns/{id}/leads
Add people. Send { "people": [...] }, or a single person object at the top level, whichever your tool produces. Up to 1000 per call. Writes.
Nothing is charged here. Everything is optional, but a video with no first name has nothing to personalize.
The reply carries each person's id and their page URL, both usable immediately. The page is live before the video exists: it shows a personalized holding screen until the render lands, so you can write the link into a row on this first call and never poll for it.
Person
firstName | string | Spoken in the video. Up to 80 characters. |
company | string | Spoken in the video. Up to 120 characters. |
email | string | Returned in your export. We never send to it. |
website | string | Their site. |
role | string | Their job title. |
profileUrl | string | A profile link, shown on their page. |
hook | string | One personal line, read aloud in your voice. Up to 200 characters, which is about 12 seconds. |
ctaLabel | string | This person's button text, overriding the recording's. Up to 40 characters. |
ctaUrl | string | Where their button goes. A URL, or a bare email address, which becomes a mailto. |
headline | string | The line under the title on their page. Supports {name} and {company}. Up to 160 characters. |
Response
added | number | Rows actually inserted. |
skipped | number | People already in this campaign. |
total | number | Everyone in the campaign now. |
people | array | Everyone this call was about, added or already there: id, firstName, company, url, videoUrl. |
{
"added": 1, "skipped": 0, "total": 42,
"people": [
{ "id": "b77d0a89-e98f-46a0-a3f3-efadddacb434",
"firstName": "Sofia", "company": "Grab",
"url": "https://vid.personade.com/p/sofia-lbGRs_NOzRrpuOU4",
"videoUrl": "https://vid.personade.com/p/sofia-lbGRs_NOzRrpuOU4/video" }
]
}skipped is people already in this campaign. They are not added again, because again means a second video and a second credit, but they still come back in people with the id and URL they already had. Re-running the same row is safe and returns the same answer.
GET/campaigns/{id}/estimate
What generating would cost. Charges nothing.
Response
leads | number | People who would be charged for: never-rendered ones only. |
low | number | Credits if the lookup surcharge is already paid. |
high | number | Credits if it is not. |
balance | number | Credits you hold now. |
affordable | boolean | Whether the balance covers the high end. Branch on this one. |
POST/campaigns/{id}/preview
Render the first few so somebody can watch one. Charges. Writes.
{ "campaignId": "3b7e...", "made": 3, "creditsCharged": 3 }POST/campaigns/{id}/generate
Charge for everyone still waiting and start the renders. This is the one that spends. Writes.
{ "campaignId": "3b7e...", "started": 40, "creditsCharged": 48 }creditsCharged is what was taken, not what was quoted. That is the number to repeat to a person.
GET/campaigns/{id}
Where a campaign has got to. Poll this every 30 seconds or so while renders run.
Response
id | string | The campaign. |
name | string | What you called it. |
status | string | One of eight values. See Polling and delivery. |
counts | object | total, pending, rendering, complete, failed, cancelled, skipped. |
stages | object | In-flight videos by stage: queued, tts, lipsync, compose, uploading. Open-ended. |
stalled | object? | Present only when our side has stopped moving: minutes, jobs, message. |
{
"id": "3b7e...", "name": "Series A ops leads", "status": "rendering",
"counts": { "total": 40, "pending": 0, "rendering": 12, "complete": 28,
"failed": 0, "cancelled": 0, "skipped": 0 },
"stages": { "lipsync": 9, "compose": 3 }
}stalledappears when our side has stopped moving. Wire an alert to it: from the outside a dead queue and a slow one look identical, and without this a poller reports "28 of 40 done" in a patient tone until somebody notices. Full detail on Polling and delivery.
GET/leads/{id}
One person, by the id you got when you added them. Use this rather than filtering the campaign list: an id cannot be matched to the wrong person, and two Daniels at two companies can.
Response
id | string | Theirs. |
firstName | string | null | As you supplied it. |
company | string | null | As you supplied it. |
campaignId | string | The campaign they are in. |
status | string | pending, rendering, complete, failed, cancelled or skipped. |
url | string | null | Their page. Live from the moment they were added. Null only if the page expired or was disabled. |
videoUrl | string | null | The MP4, once there is one. |
videoReady | boolean | Whether the video is actually there yet. The field to branch on. |
{
"id": "b77d0a89-...", "firstName": "Sofia", "company": "Grab",
"campaignId": "3b7e...", "status": "pending",
"url": "https://vid.personade.com/p/sofia-lbGRs_NOzRrpuOU4",
"videoUrl": "https://vid.personade.com/p/sofia-lbGRs_NOzRrpuOU4/video",
"videoReady": false
}GET/campaigns/{id}/videos
The finished links, up to 500. Only videos that are done, on pages that still work.
Useful for collecting a whole campaign at the end. For one person, use GET /leads/{id} instead: this returns the campaign as a list, so picking one out means matching on name and company, which is a guess.
Each video
firstName | string | null | As you supplied it. |
company | string | null | As you supplied it. |
url | string | The page to send. This is the deliverable. |
videoUrl | string | The file itself, for watching rather than sending. |
POST/campaigns/{id}/stop
Cancel what has not started and refund it. Writes.
{ "campaignId": "3b7e...", "stopped": 22,
"stillRendering": 3, "status": "stopped" }stillRenderingis the number that cannot be recalled. They finish and their credits stand. Say it out loud: "I stopped it" while three videos are on their way to real prospects is the kind of true-ish answer that costs somebody their credibility rather than only their credits.
Start building
Ready to build?
Make a key in Personade,
or have us walk you through the integration on a call.