Voice and media
Voices, voice changing, multi-speaker recast, dubbing, video import, captions, trimming, audio tools and social publishing, all as REST endpoints on Nodaro.
The voice and media endpoints expose Nodaro's voice and media tools as plain REST. They cover the voice catalog, voice changing and multi-speaker recasting, dubbing, voice design, video import, captions, trimming, audio tools, transcription and social publishing. Most of them are jobs: the POST returns { jobId } at once, you poll GET /v1/jobs/:id/status until the status is completed, and you read the result from output_data.
The routes work on every edition, with the exceptions marked below, and take a bearer token. See Authentication, and Jobs for polling many jobs at once. To send local files, upload them first; see Uploads.
Voices
| Method | Path | What it does |
|---|---|---|
GET | /v1/voices | The premade voice catalog: name, voice_id, gender, accent and age. |
GET | /v1/voices/library | Search the shared Voice Library with search, gender, language, page, page_size and more. |
GET | /v1/voice-clones | The voice clones you made before cloning was retired. |
PATCH | /v1/voice-clones/:id | Rename or edit one of those clones. |
DELETE | /v1/voice-clones/:id | Delete one of those clones. |
POST | /v1/voice-clones, /v1/voice-clones/from-url | Retired. Both answer 410 voice_cloning_retired. |
Wherever a route accepts a voice, pass a catalog voice's voice_id, a premade voice's name such as Rachel, or a clone's elevenlabsVoiceId. Clones made before the retirement keep working everywhere.
A Voice Library result may carry recommendedProvider: the text-to-speech model the voice is verified on. When your app has no model picker, send it as provider on Text to Speech, so the voice sounds like its preview. verifiedProviders lists every model the voice is verified on; when your app has a picker, override the user's choice only when it is not in that list. hasMore in the response drives "load more".
import { createClient, StaticTokenAuth } from '@nodaro/sdk'
const client = createClient({
baseUrl: 'https://app.nodaro.ai',
auth: new StaticTokenAuth(process.env.NODARO_API_KEY!),
})
const { voices, hasMore } = await client.voices.searchLibrary({ search: 'deep', language: 'en' })
const voice = voices[0]
await client.nodes.run('text-to-speech', {
text: 'Hello!',
voice: voice.voice_id,
voiceType: 'library',
...(voice.recommendedProvider ? { provider: voice.recommendedProvider } : {}),
})Design a voice, or speak in a described voice
| Method | Path | Body | Result |
|---|---|---|---|
POST | /v1/voice-design | { text, voiceDescription, model?, loudness?, guidanceScale?, seed?, quality?, shouldEnhance? } | A preview and a reusable voice id. |
POST | /v1/voice-remix | { text, voiceDescription } | Speech in the described voice, without cloning. |
For Voice Design, text is a preview line of 100 to 1,000 characters, loudness runs from -1 to 1 and guidanceScale from 0 to 100. For Voice Remix, text is 1 to 5,000 characters. Both return a job.
Change the voice in a recording
POST /v1/voice-changer replaces the voice in an audio track, or in a whole talking video, with one target voice. Send exactly one of audioUrl or videoUrl; when you send both, the video wins. For a video, Nodaro takes the audio out, changes the voice and puts it back on the original picture, and the job's output_data carries both videoUrl and audioUrl.
curl -X POST https://app.nodaro.ai/v1/voice-changer \
-H "Authorization: Bearer $NODARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "videoUrl": "https://cdn.nodaro.ai/uploads/talking.mp4", "voiceId": "Aria", "removeBackgroundNoise": false }'const { jobId } = await client.voices.change({
videoUrl: 'https://cdn.nodaro.ai/uploads/talking.mp4',
voiceId: 'Aria',
})nodaro voice changer --voice Aria --video https://cdn.nodaro.ai/uploads/talking.mp4 --watch| Field | What it does |
|---|---|
voiceId | Required. The target voice. |
audioUrl or videoUrl | Required, one of them. The recording to change. |
model | The speech-to-speech model. |
stability, similarityBoost | 0 to 1. How steady the delivery is, and how close to the target voice. |
style | 0 to 1, default 0. Exaggerates the delivery, at some cost in speed and steadiness. |
useSpeakerBoost | Sharpens the likeness to the target voice, a little more slowly. |
seed | An integer that makes the output repeatable. |
removeBackgroundNoise | true returns only the clean voice; false keeps the music and effects under the new voice. |
See Voice Changer for prices and tips.
Recast several speakers
Voice Changer Pro detects each speaker in a recording and gives each one a different voice, keeping the words and the timing. It runs on Nodaro Cloud. A self-hosted install connected to Nodaro Cloud can run the one-shot recast through that connection; the analyze and export steps need Nodaro Cloud itself. See Cloud connect.
| Method | Path | What it does |
|---|---|---|
POST | /v1/voice-changer-pro | Recast every mapped speaker, as a finished video or as separate stems. |
POST | /v1/voice-changer-pro/analyze | Detect the speakers without recasting. Nodaro Cloud only. |
POST | /v1/voice-changer-pro/export | Render a finished video from your own mix of the stems. Nodaro Cloud only. |
Map voices to speakers
orderedVoices maps speakers by detection order: speaker 0 gets entry 0, speaker 1 gets entry 1, and so on, with 1 to 8 entries. Speakers past the end of the list keep their own voice. Each entry is one of:
- A voice id, such as
"Rachel". null, a keep-slot: that speaker keeps their own voice, and later speakers are still recast. Keep-slots cost nothing, but at least one entry must be a voice.- An object with per-speaker settings:
{ voiceId, engine?, stability?, similarityBoost?, style?, useSpeakerBoost?, seed?, volumeMode?, volume? }.engineissts(the default, speech-to-speech) orv3, which performs the line again from the transcript with ElevenLabs v3 and takes astabilityof 0, 0.5 or 1 only.volumeModeismatch(the default),normalizeormanual, withvolumefrom 0 to 200 percent.
Before recasting, Nodaro always separates the voices from the music and effects. The rest of the body controls the result:
| Field | What it does |
|---|---|
audioUrl or videoUrl | Required, one of them. For a video, the recast audio goes back on the original picture. |
preserveBackground | Mix the music and effects back under the new voices. Default true. |
separationQuality | fast (the default, keeps more of the voice) or best (a finer split). |
removeBackgroundNoise | Also denoise the result. |
musicVolumeMode, musicVolume | The level of the kept background: match (the default), normalize, or manual with 0 to 200 percent. |
voiceFx | A reverb or an echo on the voices only: { preset, wetDryMix?, delayMs?, decay? }. Reverb presets such as room, hall or church use wetDryMix; echo and custom use delayMs and decay. |
output | video (the default, the finished result) or stems (dry, unleveled tracks for your own mix). |
analysis | A previous analyze job's output_data. The recast reuses its speakers and stems instead of detecting them again. |
The recast is charged per mapped speaker; analyze and export have flat prices. See Voice Changer Pro for the prices.
The interactive flow: analyze, recast to stems, export
The one-shot recast renders a finished video in one call. The interactive flow lets you check the speakers before you pay for the recast, and mix the result before you render it.
Detect the speakers
POST /v1/voice-changer-pro/analyze with audioUrl or videoUrl, and optionally separationQuality and suggestTitle. The finished job's output_data has the separated vocalsUrl and backgroundUrl, and the detected languageCode and languageProbability. Its speakers list gives each speaker's id, segments, firstStartSec, wordCount and a snippet. It also has a suggestedTitle when you asked for one. Look for "speakers" that are not people, such as applause.
Recast to stems
POST /v1/voice-changer-pro with output: "stems" and the whole analysis output_data as analysis. The recast skips detection, so you can recast again with other voices without paying for detection again.
Mix and export
Set the level of each stem in your own interface; that costs nothing. Then POST /v1/voice-changer-pro/export with the source videoUrl and up to 16 tracks, each { url, gain, muted, kind? }. gain is 0 to 200, kind is voice or background, and at least one track must be unmuted. An optional voiceFx applies to the voice tracks only. The video stream is copied, never encoded again, so the export matches your preview. The finished job's output_data.videoUrl is the result.
AUTH="Authorization: Bearer $NODARO_API_KEY"
VIDEO="https://cdn.nodaro.ai/uploads/panel.mp4"
# 1. Detect the speakers
JOB=$(curl -s -X POST https://app.nodaro.ai/v1/voice-changer-pro/analyze -H "$AUTH" \
-H 'Content-Type: application/json' -d "{\"videoUrl\":\"$VIDEO\",\"suggestTitle\":true}" | jq -r .jobId)
# ...poll until completed, then keep the analysis
ANALYSIS=$(curl -s "https://app.nodaro.ai/v1/jobs/$JOB/status" -H "$AUTH" | jq .data.output_data)
# 2. Recast to stems: speaker 2 keeps their own voice
JOB=$(curl -s -X POST https://app.nodaro.ai/v1/voice-changer-pro -H "$AUTH" -H 'Content-Type: application/json' \
-d "{\"videoUrl\":\"$VIDEO\",\"orderedVoices\":[\"Rachel\",null,\"Aria\"],\"output\":\"stems\",\"analysis\":$ANALYSIS}" | jq -r .jobId)
# 3. Export your mix
curl -s -X POST https://app.nodaro.ai/v1/voice-changer-pro/export -H "$AUTH" -H 'Content-Type: application/json' -d "{
\"videoUrl\": \"$VIDEO\",
\"tracks\": [
{ \"url\": \"<voice stem 0>\", \"gain\": 100, \"muted\": false },
{ \"url\": \"<voice stem 1>\", \"gain\": 90, \"muted\": false },
{ \"url\": \"<background stem>\", \"gain\": 70, \"muted\": false, \"kind\": \"background\" }
],
\"voiceFx\": { \"preset\": \"hall\", \"wetDryMix\": 25 }
}"const { jobId: analyzeJob } = await client.voices.analyze({ videoUrl, suggestTitle: true })
const analysis = (await client.jobs.get(analyzeJob)).output_data // once completed
const { jobId: recastJob } = await client.voices.recast({
videoUrl,
orderedVoices: ['Rachel', null, 'Aria'],
output: 'stems',
analysis,
})
const stems = (await client.jobs.get(recastJob)).output_data // once completed
await client.voices.exportMix({
videoUrl,
tracks: [
{ url: stems.tracks[0].url, gain: 100, muted: false },
{ url: stems.tracks[1].url, gain: 90, muted: false },
{ url: stems.backgroundUrl, gain: 70, muted: false, kind: 'background' },
],
voiceFx: { preset: 'hall', wetDryMix: 25 },
})nodaro voice analyze --video https://cdn.nodaro.ai/uploads/panel.mp4 --watch --json > analyze.json
jq .output_data analyze.json > analysis.json
nodaro voice recast --video https://cdn.nodaro.ai/uploads/panel.mp4 --voices Rachel,keep,Aria \
--analysis-file analysis.json --output stems --watch
nodaro voice export --source https://cdn.nodaro.ai/uploads/panel.mp4 --tracks-file mix.json \
--voice-fx hall --voice-fx-mix 25 --watchIn the CLI, the word keep in --voices is a keep-slot.
Dub into another language
POST /v1/dubbing translates and revoices an audio track or a whole video, keeping each speaker's voice. Send exactly one source: audioUrl, videoUrl, or sourceUrl (a public link, such as a YouTube or TikTok page, that the dubbing model fetches itself). A video comes back as a dubbed video: output_data.videoUrl, plus the dubbed track alone in output_data.audioUrl.
| Field | What it does |
|---|---|
targetLanguage | Required. An ISO code, for example es or pt-BR. |
sourceLanguage | The spoken language. Detected when you omit it. |
numSpeakers | 0 (the default) detects them; 1 to 20 improves the separation when you know the number. |
startTime, endTime | Dub only this window of the source, in seconds. |
disableVoiceCloning, dropBackgroundAudio | Do not reproduce each speaker's own voice, or drop the music and effects. |
highestResolution | Keep the source resolution on a video. |
useProfanityFilter, targetAccent, watermark | A profanity filter, an accent for the target language, and the dubbing model's watermark on a video. |
A dub is priced per minute of the dubbed span, with a 1-minute minimum. The span can be up to 30 minutes; a longer one answers 413, so dub a window with startTime and endTime. See Dubbing.
Import a video from a link
POST /v1/download-video imports a social video (YouTube, TikTok, Instagram, X or Facebook) or a direct link to a video file into your storage. It returns { downloadId }, not a job id. The finished file lands in your library.
| Field | What it does |
|---|---|
url | Required. The page or file to import. |
maxHeight | Cap the resolution, for example 720. Omit it for the best available. |
sectionStartSec, sectionEndSec | Import only that range, in seconds. Send both or neither. |
requireAudio | A result without sound fails by default, because it usually means a degraded source. Send false to accept a silent clip. |
Follow the download with GET /v1/download-video/progress/:downloadId, a stream of server-sent events. About every 500 ms it sends { phase, percent, videoUrl?, thumbnailUrl?, error? }, and it ends at completed, with the stored videoUrl, or at failed, with the error. Start listening as soon as the import starts, because the progress is kept only briefly after the download ends. At most 4 downloads run per account at once; a fifth answers 429 too_many_downloads.
DL=$(curl -s -X POST https://app.nodaro.ai/v1/download-video \
-H "Authorization: Bearer $NODARO_API_KEY" -H 'Content-Type: application/json' \
-d '{ "url": "https://www.youtube.com/watch?v=VIDEO_ID", "maxHeight": 720 }' | jq -r .downloadId)
curl -sN https://app.nodaro.ai/v1/download-video/progress/$DL \
-H "Authorization: Bearer $NODARO_API_KEY"const { downloadId } = await client.media.downloadVideo({
url: 'https://www.youtube.com/watch?v=VIDEO_ID',
maxHeight: 720,
})
for await (const event of client.media.downloadVideoProgress(downloadId)) {
console.log(`${event.phase} ${event.percent}%`)
if (event.phase === 'completed') console.log('stored at', event.videoUrl)
}nodaro media download "https://www.youtube.com/watch?v=VIDEO_ID" --max-height 720 --watch
nodaro media metadata "https://www.youtube.com/watch?v=VIDEO_ID"Two related routes answer at once:
POST /v1/video-metadatawith{ url }reads a video's duration, size, title and live status without downloading it. Use it to decide whether to import only a section.POST /v1/save-to-storagewith{ mediaUrl, filename?, mediaType? }copies any external media URL into your storage, on the server. It returns a job.
Edit video
| Method | Path | What it does | Price |
|---|---|---|---|
POST | /v1/trim-video | Cut a video to a range. | Trim Video |
POST | /v1/add-captions | Burn captions into a video. | Add Captions |
POST | /v1/still-to-video | One image and one audio track become an MP4. | 0 credits |
POST | /v1/slideshow | 2 to 100 images and an optional audio track become an MP4. | 0 credits |
POST | /v1/video-overlay | Place 1 to 20 timed image layers over a video. | 20 credits |
POST | /v1/media/process | Cut or crop a stored file, and answer at once. | Free |
Trim a video
POST /v1/trim-video takes videoUrl and the range in the unit you prefer: startTime and endTime in seconds, trimStartFrames and trimEndFrames, trimStartSeconds and trimEndSeconds, or keepFirstSeconds or keepLastSeconds.
Burn in captions
POST /v1/add-captions takes videoUrl and the words from the first source it finds, in this order: captions (word-timed entries), transcript, text on the subtitle style, then automatic transcription of the speech.
| Field | What it does |
|---|---|
style | subtitle (a static block), or a kinetic style: word-highlight, karaoke, tiktok-words, word-pop or bouncy. |
text | On subtitle: the caption itself, burned as one static block for the whole video. On a kinetic style: only the fallback when transcription finds nothing. |
captions | Word-timed entries, { text, startMs, endMs }, one per word for the kinetic styles. A transcription's words fit as they are. |
auto_transcribe, transcribe_provider | Transcribe the speech when nothing else gives the words. A kinetic style needs word timings: incredibly-fast-whisper (the default) or elevenlabs-stt. |
look | outline (the default on kinetic styles) or clean (the default on subtitle). |
maxWordsPerLine | 1 to 20 words per line, on top of the width of the frame. 1 or 2 gives a punchy read. |
highlightColor, animate | Kinetic styles only; 400 on subtitle. |
position, positionY, fontSize, fontFamily, fontWeight, color, backgroundColor, strokeColor, strokeWidth, uppercase | The look of the text. |
segments | Give ranges of the video their own style, look and position. |
A plain-text subtitle with no styling is the cheap render. A kinetic style, any styling field, timed captions, automatic transcription or segments renders at the higher price and keeps the source frame rate. See Add Captions for every option and both prices.
Turn stills into video
POST /v1/still-to-videotakes{ imageUrl, audioUrl, motion?, intensity?, resolution?, aspectRatio?, fps?, fit?, padColor? }. The video lasts as long as the audio; there is no duration field.motionisnone(the default),zoom-in,zoom-out,pan-left,pan-rightorken-burns, at anintensityfrom 1 to 10. See Still to Video.POST /v1/slideshowtakesimageUrls(2 to 100), an optionalaudioUrl,imageDurations(seconds per image,nullfor automatic) orperImageDuration, atransitionand the same look fields. With audio, the slideshow lasts as long as the audio; pinned durations that do not add up are scaled, and the job output says so. Without audio, it lasts the number of images timesperImageDuration, in silence. See Slideshow.
Both render on the server with no AI model and cost 0 credits. resolution is 720p, 1080p or 4K, fps is 24 or 30, and fit: "contain" letterboxes with padColor instead of cropping.
Overlay images on a video
POST /v1/video-overlay places 1 to 20 image layers over a video in one render and keeps the base audio untouched. Each layer is { imageUrl, start, end?, preset?, corner?, anchor?, x?, y?, width?, height?, fit?, opacity?, animate?, zIndex? }. start and end are seconds; without end, the layer lasts until the video ends. preset is card, corner-badge or full-frame, and an explicit position or size overrides it. A layer with neither is a corner badge, bottom-right unless corner says otherwise.
outputAspect (16:9, 9:16, 1:1 or 4:5) changes the frame, with baseFit and backgroundColor. The job returns { videoUrl, thumbnailUrl, width, height, durationSec, warnings }. It costs 20 credits per run, whatever the number of layers, and each user may send 30 requests per minute. See Video Overlay.
Cut or crop a stored file at once
POST /v1/media/process cuts or crops a file you stored and answers in the same request, for free. The body is { sourceUrl, type, trim?, crop?, format?, deleteSource? }: type is video or audio, trim is { startTime, endTime }, crop is { x, y, width, height }, and format is mp4, webm, mp3, wav, m4a or aac. It returns { data: { url, thumbnailUrl, assetId, sizeBytes, mimeType, metadata } }. deleteSource: true deletes the source afterwards, when it is yours and nothing else uses it.
Edit audio
Each route returns a job.
| Method | Path | Body | What it does |
|---|---|---|---|
POST | /v1/audio-separation | { audioUrl, mode?, quality? } | Split a track: vocal_instrumental (the default) or full stems, at auto, fast or best quality. See Audio Separation. |
POST | /v1/audio-isolation | { audioUrl } | Keep the main voice and remove the background. See Voice Extractor. |
POST | /v1/audio-fx | { audioUrl, preset?, mix?, delayMs?, decay?, eqLow?, eqHigh? } | A reverb, echo, telephone or megaphone effect. See Audio FX. |
POST | /v1/mix-audio | { audioUrls, trackVolumes? } | Layer 2 to 20 tracks, each at 0 to 200 percent. See Mix Audio. |
POST | /v1/adjust-volume | { audioUrl or videoUrl, volume?, normalize?, fadeIn?, fadeOut? } | Change the level, normalize, or fade. See Adjust Volume. |
POST | /v1/combine-audio | { segments: [{ url, startTime?, endTime? }] } | Join segments end to end. See Combine Audio. |
POST | /v1/trim-audio | { audioUrl or videoUrl, startTime?, endTime?, audioFormat? } | Cut audio, or take it out of a video, as mp3 (the default), wav or aac. See Trim Audio. |
POST | /v1/silence-detect | { audioUrl, thresholdDb?, minSilenceMs?, padMs? } | Find the silent ranges of a recording. 10 credits. See Silence Detect. |
POST | /v1/audio-sync | { sources: [{ id, url }], reference? } | Measure the offsets between 2 to 6 recordings of one conversation. See Audio Sync. |
silence-detect defaults to thresholdDb: -35, minSilenceMs: 700 and padMs: 120, and accepts audio or video. Its output_data.json is { version, ranges: [{ startMs, endMs }], durationMs }.
audio-sync costs 10 credits per source after the first: 10 for 2 sources, 30 for 4, 50 for 6. The ids must be unique, and reference must be one of them (the first source by default). Its output_data.json is { version, reference, offsets: [{ sourceId, offsetMs, confidence, driftMsPerHour }], notes }, where a time on the reference equals the time on the source plus offsetMs. Drift is measured and noted, never corrected.
Transcribe speech
POST /v1/transcribe turns speech into text and returns a job. The body is { audioUrl, provider?, language?, diarize?, tagAudioEvents?, wordTimestamps? }, and audioUrl can also be a video.
provider | Word timings | Notes |
|---|---|---|
elevenlabs-stt | Always | The only engine that honors diarize (who spoke each word) and tagAudioEvents (laughter, applause). |
incredibly-fast-whisper | With wordTimestamps: true | Without the flag, the job succeeds, is charged, and returns phrases only. |
whisper | Never | Phrases only. wordTimestamps: true answers 400 validation_error before any credit is spent. |
Omitting provider runs whisper, so name an engine whenever you need word timings. The finished job's output_data holds text, language, words (one { text, startMs, endMs, speaker? } per word, in milliseconds), json (the normalized transcript, also in milliseconds) and, on the older engines only, segments in seconds. words drops straight into captions on POST /v1/add-captions.
curl -X POST https://app.nodaro.ai/v1/transcribe \
-H "Authorization: Bearer $NODARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "audioUrl": "https://cdn.nodaro.ai/uploads/talk.mp3", "provider": "elevenlabs-stt", "diarize": true }'const { jobId } = await client.audio.transcribe({
audioUrl: 'https://cdn.nodaro.ai/uploads/talk.mp3',
provider: 'elevenlabs-stt',
})
// once completed, burn the words in as kinetic captions
const words = (await client.jobs.get(jobId)).output_data.words
await client.media.addCaptions({
videoUrl: 'https://cdn.nodaro.ai/uploads/talk.mp4',
captions: words,
style: 'word-highlight',
autoTranscribe: false,
})nodaro audio transcribe --audio https://cdn.nodaro.ai/uploads/talk.mp3 --provider elevenlabs-stt --watch
nodaro jobs get <jobId> --json | jq '.output_data.words' > words.json
nodaro media add-captions https://cdn.nodaro.ai/uploads/talk.mp4 \
--captions-file words.json --style word-highlight --no-auto-transcribe --watchPublish to social networks
The connect flows open pop-up windows and are meant for the web app. Publishing works with a personal API token. OAuth app tokens cannot manage connections.
| Method | Path | What it does |
|---|---|---|
GET | /v1/social/providers | The supported networks and whether each is available on this deployment. |
GET | /v1/social/connections | Your connected accounts. |
DELETE | /v1/social/connections/:id | Disconnect an account. |
POST | /v1/social/telegram/connect | Connect Telegram with a bot token: { botToken }. |
POST | /v1/social/connect/custom | Connect a network that uses fields instead of a sign-in: { platform, fields }. |
POST | /v1/social/publish | Publish now. Returns a job. 10 credits. |
POST | /v1/social/scheduled-posts | Schedule a post. 1 credit, charged when it publishes. |
GET | /v1/social/scheduled-posts | Your scheduled posts, filtered by from, to and status. |
PATCH | /v1/social/scheduled-posts/:id | Edit a post while it is queued or draft. |
DELETE | /v1/social/scheduled-posts/:id | Cancel a queued post. Its history is kept. |
GET /v1/social/providers lists every network with { id, label, connectKind, editor, category, capabilities, available }. category is social (a feed you post to) or publishing (a site you publish articles on). A network the deployment has not set up is listed with available: false, never hidden. Networks that connect with fields (Bluesky, Dev.to, Hashnode, Medium, WordPress and Lemmy) describe their fields in customFields, and Nodaro checks the credential with the network before saving it.
POST /v1/social/publish takes { platform, action, connectionId?, caption?, mediaUrl or mediaItems, … }. Two failures need different handling:
503 publish_retryable: nothing was posted. Sending the identical request again is safe.500 publish_failed: the outcome is unknown. Sending again may post twice, so check the network first.
A scheduled post takes { connectionId, action, scheduledAt, caption?, media? }, where each media item is { type, r2Key or url }. The media must be files stored on this deployment, which Nodaro turns into fresh links when the post goes out; links to other sites are refused. Editing a post that is already publishing answers 409 not_editable. See Publish to Social and Publishing to social.
SDK and CLI equivalents
| Area | TypeScript SDK | CLI |
|---|---|---|
| Voices and voice changing | client.voices.list, searchLibrary, listClones, deleteClone, change, recast, analyze, exportMix, design, remix, dub | nodaro voice list, changer, recast, analyze, export, design, remix, dub, clones |
| Media | client.media.downloadVideo, downloadVideoProgress, videoMetadata, trimVideo, trimAudio, addCaptions, stillToVideo, slideshow, videoOverlay, saveToStorage, process | nodaro media download, metadata, trim-video, trim-audio, add-captions, still-to-video, slideshow, video-overlay, save |
| Audio | client.audio.separate, isolate, applyFx, mix, adjustVolume, combine, transcribe | nodaro audio separate, isolate, fx, mix, adjust-volume, combine, transcribe |
| Editing helpers | client.edit.silenceDetect, audioSync | nodaro edit silence-detect, audio-sync |
Over MCP, the same routes are tools such as voice_changer, voice_changer_pro, voice_changer_pro_analyze, voice_changer_pro_export, dubbing, transcribe, add_captions and separate_audio. See the MCP tools reference.
Errors
| Status | Code | Meaning |
|---|---|---|
400 | validation_error | A field is missing or invalid, for example wordTimestamps on whisper, a style field that needs a kinetic caption style, or an all-muted export. |
400 | provider_not_configured | The deployment has not set up that social network. |
401 | unauthorized | The token is missing, invalid or revoked. |
402 | insufficient_credits | Nodaro Cloud only. The account cannot cover the job. |
404 | not_found | A Voice Changer Pro route was called on a deployment that does not serve it. |
409 | not_editable | The scheduled post is already publishing. |
410 | voice_cloning_retired | Voice cloning is no longer offered. |
413 | — | The dubbed span is longer than 30 minutes. |
429 | too_many_downloads | 4 video imports are already running for your account. |
500 | publish_failed | The post's outcome is unknown. Check the network before you send it again. |
503 | publish_retryable | Nothing was posted. The identical request is safe to send again. |
Frequently asked questions
Related
Voice Changer Pro
Add Captions
Transcribe
Jobs
Uploads
Last updated on
Studio productions
Read and edit Studio productions over REST: apply atomic operations, generate stills and clips, land finished jobs, plan the export and share or copy a film.
Character training
Train a high-fidelity model on a character over REST, poll the training, remove the model, and learn when Generate Image uses the trained model.