@martini-film/client
v0.10.0
Published
The official JavaScript and TypeScript client for the Martini API: generation, workflow runs, projects and canvases
Downloads
754
Readme
@martini-film/client
The JavaScript and TypeScript client for the Martini API: generate media, and run saved workflows.
npm install --save @martini-film/clientRequires Node.js 18 or later.
import { createMartiniClient } from '@martini-film/client'
const martini = createMartiniClient({
apiKey: process.env.MARTINI_API_KEY,
})
const result = await martini.subscribe('nano-banana-2', {
input: {
prompt: 'A practical miniature moon base photographed on 35mm film.',
},
projectId,
canvasId,
placement: 'auto',
idempotencyKey: 'shot-42',
})
console.log(result.data.images[0].url)
console.log(result.data.olive_cost)
console.log(result.data.martini.preview_url)The client provides typed model IDs and an asynchronous queue interface for generation jobs. Submit and status
responses include the snapshotted olive_cost once pricing is available, and result responses always include it. An
overlapping retry of the same idempotent submission can briefly omit the field while the original request is still
being priced. The snapshotted cost does not change if model pricing changes later.
Non-blocking input advisories are returned in martini.warnings.
Seedance 2.0 Base, Fast, and Mini expose text, image, and reference-to-video endpoints under
bytedance/seedance-2.0, bytedance/seedance-2.0/fast, and bytedance/seedance-2.0/mini.
Seedance 2.5 exposes the same three modes under bytedance/seedance-2.5, with 4–30 second takes and Auto duration.
Grok Imagine 1.5 exposes typed text, image, and reference-to-video endpoints under
xai/grok-imagine-video/v1.5. Text and image modes support up to 1080p; reference mode accepts up to seven images
and one optional audio reference at 480p or 720p.
MiniMax H3 and H3 Max expose typed text, image, and reference-to-video endpoints under minimax/h3 and
minimax/h3-max. Reference mode accepts up to nine image, three video, and three audio references; H3 allows all
15 files together while H3 Max caps the combined total at 12. H3 Max Turbo (preview) exposes text and
image-to-video only under minimax/h3-max-turbo, with H3 Max's contract at half the cost.
Wan 3.0 and Wan 3.0 Prime expose typed text, image, and reference-to-video endpoints under alibaba/wan-3.0 and
alibaba/wan-3.0-prime. All three modes support 480p, 720p, and 1080p output with optional native audio. The Prime
contract does not expose prompt expansion, enhanced reasoning, or a provider safety-checker toggle because its
provider has no corresponding controls.
Queue URL fields returned from queue.submit(), queue.status(), and onQueueUpdate use the configured Martini API
domain, such as https://api.martini.film/fal/queue/.... The SDK keeps the Fal-compatible proxy details internal.
Direct draft lifecycle calls use Martini-owned URLs under https://api.martini.film/generation-api/requests/.
projectId and canvasId optionally override the API key's default project and canvas. If projectId is supplied
without canvasId, Martini uses the project's first canvas. A canvasId override requires projectId. placement
currently accepts auto. Use idempotencyKey to safely retry the same logical submission.
Projects and canvases
martini.projects lists where a generation, an upload, or a workflow run can land: the projects of your
organization that the key's user can see, and each project's canvases. It is the same set the MCP connector's
get_projects shows them, so an id picked here works everywhere a projectId or canvasId is accepted.
const { projects } = await martini.projects.list({ query: 'Pilot' }) // ranked: exact name or id first
const pilot = projects.find(project => project.canEdit) // canEdit: false means read-only for this key
const { canvases } = await martini.projects.canvases(pilot.id)
const canvasId = canvases.find(canvas => canvas.isDefault)!.id // where the project opens
await martini.generations.submit('nano-banana-2', { input: { prompt: '…' }, projectId: pilot.id, canvasId })list() returns the most recently joined projects first (limit 1–50, default 20; truncated says whether more
matched) and ranks them when query is given: an exact id or name first, then a name that starts with, contains,
or holds every word of the query, then an id fragment. exactName: true keeps only exact name (or id) matches.
Every project carries openInMartini, a link to it in the app, and visibility (private, link_view, or
link_edit). A project the key cannot read, or one from another organization, answers PROJECT_NOT_FOUND (404).
canvases() answers PROJECT_DOCUMENT_TOO_LARGE (409, durable — retrying will not help) when the project's document
is too large to open, and PROJECT_DOCUMENT_UNAVAILABLE (503, retry) when it could not be read.
subscribe() and queue.submit() generate immediately by default. To create an editable draft without spending olives
or contacting a provider:
const draft = await martini.queue.submit('nano-banana-2', {
input: {
prompt: 'A practical miniature moon base photographed on 35mm film.',
},
mode: 'draft',
projectId,
})
await martini.queue.generate({ requestId: draft.request_id })Draft generation uses the latest compatible settings saved on the Martini canvas. Raw @fal-ai/client callers can
create the same draft by adding the X-Martini-Mode: draft submit header.
Workflows
A workflow is a chain of Actions saved from a Martini canvas; a run is one execution of it. Runs of a saved
workflow start from empty bins, so pass asset ids for every input bin; the outputs land in your workspace's
"Workflow runs" project (or in a projectId you name). The same API key works for generation and workflows.
const { workflows } = await martini.workflows.list() // saved workflows: steps, inputs, outputs, fingerprint
const reference = await martini.assets.upload(file, { wait: true }) // lands in your API Generations project
const { run, results } = await martini.workflows.subscribe(workflowId, {
variables: { Script: 'INT. LAB - NIGHT. Sophie reads the results.' },
bins: { References: [reference.assetId] },
onUpdate: run => console.log(run.status, run.olives.generation),
})
for (const output of results.outputs) {
if (output.status === 'ready') console.log(output.filename, output.url)
}subscribe() starts the run, polls its status with jittered backoff until it settles (completed, failed, or
cancelled), then fetches the results. The pieces are also available separately:
const run = await martini.workflows.run(workflowId, {
variables: { Script: '…' },
bins: { References: [reference.assetId] },
fingerprint: workflow.fingerprint, // optional: refuse to run if the workflow changed since you read it
idempotencyKey: 'job-42',
})
const status = await martini.runs.status(run.id) // status.phase: what Martini is doing right now
const detailed = await martini.runs.status(run.id, { activity: true }) // adds each step's activity trail as events
const results = await martini.runs.results(run.id) // the takes settled so far; results.status says whether more may come
const settled = await martini.runs.wait(run.id) // poll an existing run, then fetch results
const resumed = await martini.runs.resume(run.id) // retry a failed run's failed step with the same inputs (409 unless failed)
const me = await martini.me() // connection check: your organization and key labelworkflows.list({ all: true }) appends the placed workflows you can run in place (copies on canvases you edit),
each with its current canvas inputs; workflows.run(placedId, …) then reads the canvas bins you do not pin.
workflows.create({ from, projectId }) places a copy of a saved workflow on a canvas of your choosing.
Every workflow and run carries a fingerprint: the content identity of its machinery. Pass the one you read to
run() and Martini answers WORKFLOW_CHANGED (with the current fingerprint) instead of running something else.
Input keys are names (case-insensitive) or ids. A key the workflow does not have rejects with
WORKFLOW_VARIABLE_NOT_FOUND or WORKFLOW_BIN_NOT_FOUND; a saved workflow's input bin you left out with
WORKFLOW_BIN_REQUIRED; an asset you cannot read with WORKFLOW_ASSET_NOT_FOUND. Each body lists the accepted
inputs. The run echoes what it pinned under run.variables and run.bins.
Output urls are presigned downloads valid for expiresIn seconds; fetch them promptly rather than storing them.
oliveBudget is optional: when omitted, Martini arms the spend rail per action from the plan estimate; when set, a
plan estimated over it fails the run with the estimate in run.error. Runs started over the API auto-approve, so
nobody has to click anything in the app.
engine: 'fable' (Workflow Beta) runs the whole workflow in one autonomous session instead of the per-Action harness. It needs the workflow_beta grant for your organization and an oliveBudget; resume is refused on such runs, start a new one instead. Omit it for the default engine.
Martini-native generation routes
martini.generations is the Martini-native way to generate: the same models, the same key, the same billing and
canvas placement as subscribe()/queue.*, over /v1/generations instead of the fal-compatible queue. One resource
describes a generation from draft to terminal state and carries output once status is completed, so a caller
polls one URL. Statuses are draft, pending, running, completed, failed, and cancelled.
const generation = await martini.generations.subscribe('bytedance/seedance-2.5/text-to-video', {
input: { prompt: 'A practical miniature moon base at blue hour.', duration: 'auto', resolution: '720p' },
projectId,
idempotencyKey: 'shot-42',
onUpdate: update => console.log(update.status),
})
if (generation.output) console.log(generation.output.video.url, generation.oliveCost)
else console.log(generation.status, generation.error) // failed or cancelled: returned, not thrownsubscribe() submits, polls with jittered backoff until the generation settles, and returns it — a failed or
cancelled generation is returned with its error, not thrown. The pieces are also available separately:
const submitted = await martini.generations.submit('nano-banana-2', { input: { prompt: '…' }, mode: 'draft' })
const started = await martini.generations.generate(submitted.id) // a draft never settles until it is started
const current = await martini.generations.status(submitted.id) // output is on this object once completed
const settled = await martini.generations.wait(submitted.id) // poll an existing generation until it settles
const cancelled = await martini.generations.cancel(submitted.id)
const catalog = await martini.models() // the aliases this key can invoke, with your negotiated pricingprojectId, canvasId, placement, idempotencyKey, and mode are body fields on this surface; the fal-compatible
queue carries the same options as X-Martini-* headers. Errors are MartiniApiErrors with the same stable codes as
the queue (for example INSUFFICIENT_OLIVES, RATE_LIMITED, REQUEST_ALREADY_TERMINAL).
Martini-native routes (generations.*, models(), projects.*, workflows.*, runs.*, me(), queue.generate(),
assets.*) reject with MartiniApiError, which carries status, the stable code, and the parsed body:
import { MartiniApiError } from '@martini-film/client'
try {
await martini.runs.status(runId)
} catch (error) {
if (error instanceof MartiniApiError && error.code === 'WORKFLOW_RUN_NOT_FOUND') {
// unknown run, or one from another organization
}
}The Fal-compatible queue calls (subscribe(), queue.submit(), queue.status(), queue.result(),
queue.cancel()) keep throwing ApiError.
Reference-to-video binding
Reference arrays are positional, and array order determines each 1-based token. Image bindings are recommended; multiple-video and all audio bindings are required:
| Input field | Prompt token | Meaning |
| --- | --- | --- |
| image_urls | @ImageN | @Image1 is the first image URL. |
| video_urls | @VideoN | @Video1 is the first video URL. |
| audio_urls | @AudioN | @Audio1 is the first audio URL. |
Each media type has its own numbering. Bare names and labels do not bind media: write @Image1 is Norman, not
NORMAN. One URL is one reference, so a contact sheet remains one image rather than one reference per panel. If one
sheet repeats a person across views, state that every panel depicts the same single character.
const result = await martini.subscribe('bytedance/seedance-2.5/reference-to-video', {
input: {
prompt:
'@Image1 is the lead character. @Image2 is the laboratory. Keep exactly one instance of @Image1. Inside @Image2, @Image1 dodges the attack. Follow the camera movement from @Video1 and time the impact to @Audio1.',
image_urls: [characterUrl, locationUrl],
video_urls: [cameraGuideUrl],
audio_urls: [impactAudioUrl],
duration: '8',
resolution: '720p',
aspect_ratio: '16:9',
generate_audio: true,
},
})Only tagged audio references are used; generate_audio independently controls generated audio. Image tokens are
recommended but not required: unbound images are still submitted, and the response reports the advisory in
martini.warnings, because Seedance may not use them. Multiple video references and every audio reference must have
matching tokens. Out-of-range tokens and duplicate URLs fail validation. Seedance 2.0 requests accept up to 9 images,
3 videos, and 3 audio files, with 12 files total. Seedance 2.5 requests accept up to 30 images, 10 videos, and 10 audio
files, with 50 files total. Use image-to-video for start/end frames and reference-to-video for reference arrays.
Keep the action achievable within the selected duration. Split prompts with many cuts, dialogue lines, entrances, and impacts into separate generations when their order must remain clear.
Base64 media inputs
Media URL fields accept public HTTPS URLs, Martini-hosted URLs, or base64 data URLs. Put the data URL directly in the normal field; there is no separate base64 property or upload call.
const result = await martini.subscribe('bytedance/seedance-2.0/reference-to-video', {
input: {
prompt: 'Follow the camera movement from @Video1.',
video_urls: [`data:video/mp4;base64,${videoBase64}`],
},
})Limits apply to decoded or fetched media: 10 MB per image, 30 MB per MP4 video reference, and 15 MB per MP3 or WAV
reference. The complete request body is limited to 72 MiB. Base64 adds roughly 33% overhead, so use HTTPS URLs for
large or multiple references. Binary File and Blob values are not uploaded automatically.
Supported methods:
subscribe()queue.submit()queue.status()queue.result()queue.generate()queue.cancel()assets.upload()assets.prepareUpload()assets.completeUpload()assets.get()assets.uploadBase64()projects.list()projects.canvases()workflows.list()workflows.get()workflows.create()workflows.run()workflows.subscribe()runs.status()runs.results()runs.wait()generations.submit()generations.status()generations.cancel()generations.generate()generations.wait()generations.subscribe()models()me()
The API key is a server-side secret. Do not embed it in browser bundles or public environment variables.
Upload reusable project assets
Uploads create visible canvas assets in the API key's default project and canvas. Use the returned stable Martini URL in later generation requests:
const asset = await martini.assets.upload(file, {
filename: 'reference.mp4',
contentType: 'video/mp4',
projectId,
canvasId,
})
const result = await martini.subscribe('bytedance/seedance-2.0/reference-to-video', {
input: {
prompt: 'Follow the camera movement from @Video1 for a nighttime city shot.',
video_urls: [asset.url!],
},
})For large files, assets.upload() performs the same explicit flow you can call manually: prepare a create-only
presigned upload URL, PUT bytes directly to Martini storage, complete the upload, then poll asset status with jittered
backoff until processing finishes. Completion can return queued while Martini waits for a bounded upload-processing
worker. Upload destination overrides must provide projectId and canvasId together.
Changelog
0.10.0
- Breaking:
needs_revisionis a terminalMartiniWorkflowRunStatusbesidecompleted,failed, andcancelled: the agent stopped on purpose because the inputs fail a precondition the brief sets. The same value is thestatusof the stopped action and of each item the agent ended (the formerdeclineditem status is gone). Earlier versions keep polling such a run until their timeout — upgrade before Martini ships the status.outcomeandrevisionare unchanged;POST /v1/runs/{runId}/resumeaccepts aneeds_revisionrun. - The status summary's
outputsgainsneedsRevision, the count of units the agent ended that way.
0.9.0
martini.projects.list({ query?, exactName?, limit? })(GET /v1/projects) lists the organization's projects the key's user can see, ranked byquery, each withcanEdit,visibility, andopenInMartini.martini.projects.canvases(projectId)(GET /v1/projects/{projectId}/canvases) lists a project's canvases in the app's order withisDefaulton the first. Use them to pick theprojectId/canvasIdfor a generation, an upload, or a run. New types:MartiniProject,MartiniProjectList,MartiniCanvas,MartiniCanvasList,MartiniProjectVisibility,MartiniProjectListOptions,MartiniProjectClient.- New error codes from
projects.canvases():PROJECT_DOCUMENT_TOO_LARGE(409) andPROJECT_DOCUMENT_UNAVAILABLE(503).
0.7.1
- The run object carries
outcomeon every action and item:nulluntil settled, thengenerated,failed, orneeds_revision.needs_revisionmeans the agent stopped before generating because the inputs fail a precondition the brief sets; the item'srevision({ reason, inputRefs }) says what to change. The row'sstatusstaysfailed, so existing polling loops need no change. New types:MartiniWorkflowOutcome,MartiniWorkflowRevision.
0.7.0
Breaking: the Workflows API now has two nouns, workflows and runs (no instances).
workflows.run(workflowId, options)runs a saved workflow directly;options.instanceis gone. Passbinsfor every input bin (WORKFLOW_BIN_REQUIREDotherwise) and, optionally,projectId/canvasIdto choose where the run lands.- Run status and results moved to
martini.runs.status(runId),martini.runs.results(runId), andmartini.runs.wait(runId)(GET /v1/runs/{runId}[/results]).workflows.subscribe()is unchanged. workflows.list({ all: true })appends placed workflows;workflows.create({ from, projectId })places a copy.- The run object:
workflowIdnow names the placed copy that ran (only for copies you can see), the saved workflow issavedWorkflow { id, version }, and every workflow and run carries afingerprint; pass it torun()to getWORKFLOW_CHANGEDwhen the workflow changed since you read it.openInMartinilinks to the run's canvas. - Removed error codes:
WORKFLOW_INSTANCE_NOT_FOUND,WORKFLOW_INSTANCE_AMBIGUOUS. Added:WORKFLOW_CHANGED,WORKFLOW_BIN_REQUIRED,WORKFLOW_CREATE_REJECTED,WORKFLOW_CREATE_FAILED,PROJECT_NOT_FOUND.
0.6.0
workflows.run()accepts per-runvariablesandbins.
0.5.0
- First release with
workflows.*andme().
