TypeScript / JavaScript client for the Nureta ("tokenstore") Generation API — a Seedance-compatible platform for generating video, images, and scripts from text and reference images, with first-class support for explicit / NSFW content.
- Website & dashboard: https://developer.nureta.ai/
- Base URL:
https://developer.nureta.ai - Auth: API key as a bearer token (
Authorization: Bearer sk-...) - Billing: pay-per-generation against your balance (USDC top-ups on Base); failed jobs are auto-refunded
- Async by design: you create a task, then poll it (or receive a webhook) until it's
succeeded/failed
Zero runtime dependencies — built on the platform fetch. Works in Node 18+, Deno, Bun,
Cloudflare Workers, and other modern runtimes. Ships ESM + CommonJS + type declarations.
The api.md file is the method reference; openapi.yml is the
API's source of truth. This SDK mirrors the Python SDK.
npm install nuretaimport { Nureta } from 'nureta';
const client = new Nureta({ apiKey: 'sk-...' }); // or set NURETA_API_KEYCreate an API key in the API keys section of the dashboard. New keys default to 10 req/s
(adjustable 1–100). Every call counts toward that key's rate limit; a 429 RateLimitError
means back off and retry.
import { Nureta } from 'nureta';
const client = new Nureta({ apiKey: 'sk-...' });
const { id } = await client.videos.create({
model: 'seahorse-1080p',
content: [{ type: 'text', text: 'a seahorse dancing through neon waves' }],
ratio: '16:9',
duration: 8, // single clip: 5 / 8 / 10 / 12 / 15s
});
// Poll until the task finishes (succeeded / failed / cancelled):
const task = await client.videos.waitForCompletion(id, { pollIntervalMs: 3000 });
if (task.status === 'succeeded') {
console.log(task.content?.video_url);
} else {
console.log('failed:', task.error?.message); // auto-refunded
}Prefer to drive the poll loop yourself? retrieve and task.step are all you need:
for (;;) {
const task = await client.videos.retrieve(id);
if (task.status !== 'pending') break;
// task.step?.code = queued | generating_script | rendering | stitching | ...
await new Promise((r) => setTimeout(r, 3000));
}content mixes exactly one text prompt with up to 9 image_url references and at
most one audio_url:
const content = [
{ type: 'text', text: 'cinematic, moody lighting' },
{ type: 'image_url', image_url: { url: 'https://.../ref.jpg' } },
{ type: 'image_url', image_url: { url: 'https://.../start.jpg' }, role: 'first_frame' },
{ type: 'image_url', image_url: { url: 'https://.../end.jpg' }, role: 'last_frame' },
] as const;A pinned first_frame/last_frame is mutually exclusive with reference images.
Don't host your reference yet? Presign an upload:
const signed = await client.uploads.create({ fileName: 'ref.jpg', contentType: 'image/jpeg' });
// PUT the bytes to signed.uploadUrl with the same Content-Type, then use signed.assetUrl.
await fetch(signed.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': 'image/jpeg' },
body: bytes,
});const { id } = await client.images.create({
model: 'seahorse-image',
content: [{ type: 'text', text: 'a clean studio product shot on marble' }],
size: '4K', // 1K / 2K / 4K
});
const task = await client.images.waitForCompletion(id);
console.log(task.content?.image_url);Generate just a storyboard (title, characters, per-scene beats). Flat $0.01.
const { id } = await client.scripts.create({
model: 'seahorse-script',
content: [{ type: 'text', text: 'a lonely lighthouse keeper finds a message in a bottle' }],
duration: 30, // 5–180s; scales scene count, not price
});
const task = await client.scripts.waitForCompletion(id);
console.log(task.content?.script?.title, task.content?.script?.scenes);const client = new Nureta({
apiKey: 'sk-...',
baseURL: 'https://developer.nureta.ai', // override for staging
timeout: 60_000, // per-request, ms
maxRetries: 2, // idempotent requests only (see below)
defaultHeaders: { 'X-My-App': 'demo' },
fetch: myFetch, // custom fetch (proxy, instrumentation)
});Every method also takes per-call overrides:
await client.videos.retrieve(id, { timeout: 10_000, maxRetries: 5, signal: ac.signal });Requests retry with exponential backoff + jitter and honor a Retry-After header. Retries
fire on 408 / 409 / 429 / 5xx and connection errors — but only for idempotent methods
(GET polling and PUT edits). A billed create / render / cancel POST is never
silently re-submitted, so you can't be double-charged. Set maxRetries: 0 to disable.
const { data, response } = await client.models.list().withResponse();
console.log(response.headers.get('x-request-id'), data.models.length);API errors throw an APIError subclass carrying status, code, body, and headers:
import { APIError, RateLimitError } from 'nureta';
try {
await client.videos.create(params);
} catch (err) {
if (err instanceof RateLimitError) {
// back off
} else if (err instanceof APIError) {
console.error(err.status, err.code, err.message); // e.g. 401 AuthenticationError
}
throw err;
}| Class | When |
|---|---|
AuthenticationError (401) |
missing / invalid API key |
BadRequestError (400), UnprocessableEntityError (422) |
invalid request field |
PermissionDeniedError (403), NotFoundError (404), ConflictError (409) |
— |
RateLimitError (429) |
rate limit exceeded |
InternalServerError (5xx) |
generation failed (auto-refunded) |
APIConnectionError / APIConnectionTimeoutError |
network / timeout |
APIUserAbortError |
you aborted via AbortSignal |
Nureta renders explicit adult video. The underlying model has no built-in knowledge of
sex acts, so explicit shots route through a separate anatomy-reference pipeline you opt
into per shot with the explicit flag. It applies to multi-shot scenes — advanced
mode (segments) or edit mode, video only, total duration > 15s.
explicit: true— this shot depicts sex. Describe the act concretely in the shot'sscript/beat, or it renders soft (no references attach).explicit: false— build-up / story shot, rendered from text only.
Best model: seahorse-1080p for explicit video; seahorse-image at 4K for stills.
const { id } = await client.videos.create({
model: 'seahorse-1080p',
ratio: '9:16',
content: [{ type: 'text', text: 'a moonlit rooftop encounter' }],
segments: [ // must total 16–180s; per-segment 4–15s
{ script: 'she steps onto the rooftop, city lights behind her', duration_seconds: 8 },
{ script: 'he joins her at the railing; they kiss', duration_seconds: 10, explicit: false },
{ script: '<concrete description of the act>', duration_seconds: 12, explicit: true },
],
});Submit with mode: 'edit'; the task charges at submit and parks at status: 'planned' with a
scene draft. Then tweak and render:
const { id } = await client.videos.create({
model: 'seahorse-1080p',
duration: 60,
mode: 'edit',
content: [{ type: 'text', text: 'a slow bedroom scene' }],
});
const draft = await client.videos.waitForCompletion(id); // stops at status "planned"
// Tweak a shot's script / duration / pinned frames (matched by 1-based index):
await client.videos.editPlan(id, {
segments: [{ index: 2, script: 'she turns toward him', duration_seconds: 8 }],
});
// Or edit the short beats and let the server re-expand (returns 202):
await client.videos.editPlanStructured(id, {
world: 'candlelit loft, warm tones',
segments: [{ index: 3, beat: '<concrete act>', explicit: true }],
});
// Task status stays "planned" during re-expansion, so wait on the scene, not status:
await client.videos.waitForReexpand(id); // scene.reexpandStatus → "ready"
await client.videos.render(id); // commit to the full render
// or client.videos.cancel(id); // immediate full refundLegal/safety: explicit generation is for adult (18+) content you are authorized to create.
callback_urls must be HTTPS and must not target private/loopback ranges.
for (const m of (await client.models.list()).models) {
console.log(m.id, m.resolution, m.price_usd_per_sec);
}
// image / script model lists: client.images.listModels(), client.scripts.listModels()| Model | Resolution | USD/sec | USD/5s |
|---|---|---|---|
seahorse-480p |
480p | $0.1888 | $0.9440 |
seahorse-720p |
720p | $0.2773 | $1.3864 |
seahorse-1080p |
1080p | $0.5428 | $2.7139 |
seahorse-image |
1K/2K/4K | — | $0.05 / image |
seahorse-script |
— | — | $0.01 / script |
Pass callback_url (and optional callback_secret) on creation and Nureta POSTs the terminal
task object to you when it finishes — same JSON as retrieve. Delivery is at-least-once
(dedupe by id); when a secret is set, verify the X-Tokenstore-Signature HMAC-SHA256 over
timestamp + "." + rawBody. Webhook receiving is your server's job, not this SDK's.
npm install
npm run build # tsup → dist/ (esm + cjs + d.ts)
npm run typecheck
npm test # offline tests (node:test, injected fetch)