Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
TypeScript SDK

Studio productions

Create Studio productions from a plan, edit them with operations, generate stills and clips, review planned frames, and share or copy them from TypeScript.

Available on Nodaro Cloud

A Studio production is a Nodaro workflow whose settings hold the shots of a film. Each shot has a framed still, an optional animated clip, and the plan, looks, cast bindings and voice that made them. client.studio reads and writes productions, so a script, an AI assistant and the Studio app work on one production. client.shots stores the shared shot records behind share links. The methods call the Studio productions REST API. See Studio productions over MCP for the authoring guide.

Studio productions run on Nodaro Cloud. Where the routes are not served, every method throws NotFoundError. To check once, call client.studio.productions.list(): a deployment with productions answers an empty page, and one without them throws NotFoundError.

Two sets of methods

client.studio has two layers. Both work on the same productions.

LayerUse it forReturns
client.studio.productions.*The production document: create from a plan, edit with operations, generate stills and clips, add voice and music, share and copyThe payload itself
client.studio.*Planned frames: capabilities, keyframe generation and review, bundles, editor saves and link sharing with revision checksThe API's { data } envelope

The envelopes are typed; the production document is not. A production, a shot and an operation are open JSON, Record<string, unknown>. Everything you branch on is typed: version, rebased, receipts, warnings, a quote's credits and a run's jobIds. The operation vocabulary comes from the server: read it from skill().

Methods of client.studio.productions

MethodWhat it does
skill()Read the authoring guide, catalog, plan schema and operating guide
validatePlan(plan)Check a plan, for free
list(opts?)List your productions
get(productionId, opts?)Read a production
exportPlan(productionId, opts?)Plan and price the export steps
create(input?)Create a production, optionally from a plan
ops(productionId, input)Apply, or preview, a batch of operations
reconcile(productionId)Land finished generations
importPlan(productionId, plan, opts?)Add a plan's scenes to a production
describe(productionId, input)Turn a brief into scenes
generate(), generateStill(), generateClip()Frame or animate a shot
frame(productionId, input)Take a still from a shot's clip
voice(productionId, input)Speak a shot's line
revoice(productionId, input)Recast the voices of a shot's clip
music(productionId, input)Score the film
share(), unshare(), clone()Open or close the share link, or copy the production

client.studio.productions

productions.skill()

Returns the authoring guide, the full catalog, the plan's JSON Schema and the operating guide, rendered from the version the server runs. It is free.

skill(): Promise<{ skill: string; catalog: string; schema: Record<string, unknown>; operating: string; generatedFrom: object }>
const { skill, schema, operating } = await client.studio.productions.skill()

operating lists the operations ops() accepts. Read it at run time instead of hard-coding the vocabulary.

productions.validatePlan(plan)

Checks a plan before it becomes a production. It is free, stores nothing, and resolves cast names against your library. Loop on errors until valid is true, then call create({ plan }).

validatePlan(plan: Record<string, unknown>): Promise<{
  valid: boolean
  errors: Array<{ path: string; message: string; hint?: string }>
  warnings: Array<{ path: string; message: string; hint?: string }>
  summary?: { name?: string; scenes: number; shots: number; cast: number; bound: number }
}>

Prop

Type

const check = await client.studio.productions.validatePlan(plan)
if (!check.valid) console.log(check.errors)

summary.bound counts the cast entries that matched a character in your library.

productions.list(opts?)

Lists your productions, newest first.

list(opts?: { limit?: number; cursor?: string; includeArchived?: boolean }): Promise<{ data: StudioProduction[]; nextCursor?: string }>

Prop

Type

const { data: productions } = await client.studio.productions.list({ limit: 20 })

productions.get(productionId, opts?)

Reads a production. It is a pure read and never lands a finished job, so call reconcile() first when you wait for one.

get(productionId: string, opts?: { detail?: "summary" | "full"; shotId?: string }): Promise<StudioProduction>

Prop

Type

const production = await client.studio.productions.get(productionId, { detail: "full" })

productions.exportPlan(productionId, opts?)

Returns the ordered steps that assemble the film, with their prices. It runs nothing and spends nothing: run the steps yourself with the ordinary node methods.

exportPlan(productionId: string, opts?: { upscale?: boolean }): Promise<{
  canExport: boolean
  steps: Array<{ id: string; label: string; node: string; creditModel: string; credits: number | null; params: Record<string, unknown> }>
  resultStepId: string | null
  estimate: number | null
  unpriced: string[]
}>

Prop

Type

const plan = await client.studio.productions.exportPlan(productionId)
console.log(plan.canExport, plan.estimate)
for (const step of plan.steps) console.log(step.id, step.label, step.node, step.credits)

canExport is false when the production has fewer than two clips. estimate is null when any step has no price, because a partial sum would understate the cost, and unpriced names those steps' models. Each step's node is a node type, such as merge-video-audio, combine-videos or video-upscale.

productions.create(input?)

Creates a production, and optionally lands a plan in the same call.

create(input?: { name?: string; plan?: Record<string, unknown> }): Promise<{
  production: StudioProduction
  warnings?: Array<{ path: string; message: string; hint?: string }>
  summary?: { shotsAdded: number; castEnrolled: number; castBound: number }
}>

Prop

Type

const { production, summary } = await client.studio.productions.create({ name: "Rome chase", plan })

productions.ops(productionId, input)

Applies a batch of operations to a production. Every change is an operation, addressed by a stable key, such as a shot id, a role slug or a result's job id, never by position.

ops(productionId: string, input: StudioOpsRequest): Promise<StudioOpsResponse>
ops(productionId: string, input: StudioOpsRequest & { dryRun: true }): Promise<StudioOpsDryRunResponse>

Prop

Type

const result = await client.studio.productions.ops(productionId, {
  ops: [/* operations from the operating guide */],
  baseVersion: version,
  clientRequestId: crypto.randomUUID(),
})
version = result.version // carry it forward as the next baseVersion
for (const r of result.receipts) console.log(r.summary)
  • Atomic. One bad operation refuses the whole batch with a StudioOpError whose opIndex names it, and nothing is written.
  • Rebased. Two people can edit one production at once. A batch composed against an older version still applies to the newest one, and rebased is true.
  • Receipts. receipts has one past-tense line per operation, such as "Deleted take 2 of Shot 1 (in the bin)". Where an operation's effect reaches past what it names, its impact lists the keyframeIds and shotIds to refresh.
  • Adopt the answer. Replace your copy with production and carry version forward. Do not merge into your old copy.

Preview a batch. With dryRun: true, the answer says what the batch would do, so a person can approve an assistant's edits first. It has dryRun, version, receipts and warnings, and no production. Each receipt adds class: S safe, D deletes, P changes who can reach the work, $ spends credits. It also adds restorable, which is present only when the operation put something in the bin. Read it as restorable ?? false.

Write dryRun: true as a literal in the call's own object. Passed through a variable, it widens to boolean, and the call types as an apply while it still previews.

A preview sends two requests: first an empty batch that proves the deployment can preview, then your batch. A deployment that cannot preview would otherwise apply your batch without warning. Two errors can result:

import { StudioPreviewAppliedError, StudioPreviewUnavailable } from "@nodaro/sdk"

try {
  const preview = await client.studio.productions.ops(productionId, { ops, baseVersion, dryRun: true })
  for (const r of preview.receipts) console.log(r.class, r.summary, r.restorable ?? false)
} catch (err) {
  if (err instanceof StudioPreviewUnavailable) {
    // Nothing was sent. Say that no preview is available; do not apply the batch instead.
  } else if (err instanceof StudioPreviewAppliedError) {
    // The batch was applied. Adopt err.applied.production and err.applied.version.
    // Do not send it again. When err.applied is undefined, read the production first.
  } else {
    throw err
  }
}

productions.reconcile(productionId)

Lands every generation that finished since you last looked, and reports what is still running. It is the one call that turns finished jobs into results without the app open, and it writes only when something landed.

reconcile(productionId: string): Promise<{
  landed: string[]
  pending: string[]
  failed: string[]
  warnings: string[]
  production: StudioProduction
  version: number
}>

Prop

Type

const { landed, pending } = await client.studio.productions.reconcile(productionId)

landed lists the jobs whose media is now on the production, pending the jobs still running, and failed the jobs that failed or were cancelled.

productions.importPlan(productionId, plan, opts?)

Adds a plan's scenes to an existing production.

importPlan(productionId: string, plan: Record<string, unknown>, opts?: { mode?: "append" }): Promise<{
  production: StudioProduction
  warnings?: Array<{ path: string; message: string; hint?: string }>
  summary?: { shotsAdded: number; castEnrolled: number; castBound: number }
}>

Prop

Type

await client.studio.productions.importPlan(productionId, extraScenesPlan)

productions.describe(productionId, input)

Turns a brief into scenes with the Director. It starts a job and returns at once; the scenes land through reconcile(). The production comes back with the run recorded as a pending draft.

describe(productionId: string, input: {
  brief: string
  llmModel: string
  mode?: "append" | "replace"
  label?: string
  clientRequestId?: string
}): Promise<{ production: StudioProduction; jobId: string }>

Prop

Type

const { jobId } = await client.studio.productions.describe(productionId, {
  brief: "A courier races across Rome in the rain to deliver a violin.",
  llmModel,
})

Generate stills and clips

generateStill() frames a shot, generateClip() animates it, and generate() does either by kind. A run submits the jobs, records a pending marker on the production, and returns: nothing waits for minutes. The request is built on the server from the shot's own plan, looks and references, so a script and a click in the app produce the same media.

generate(productionId: string, input: StudioGenerateRequest): Promise<StudioGenerateResult>
generateStill(productionId: string, shotId: string, opts?: StudioGenerateOptions): Promise<StudioGenerateResult>
generateClip(productionId: string, shotId: string, opts?: StudioGenerateOptions): Promise<StudioGenerateResult>

Prop

Type

import { isStudioGenerateEstimate } from "@nodaro/sdk"

const quote = await client.studio.productions.generateStill(productionId, "shot-2", { count: 2, dryRun: true })
if (isStudioGenerateEstimate(quote)) console.log(quote.credits) // null means unpriced, not free

const run = await client.studio.productions.generateStill(productionId, "shot-2", {
  count: 2,
  clientRequestId: crypto.randomUUID(),
})
  • Quote first. dryRun: true prices the run and writes nothing. Narrow the answer with isStudioGenerateEstimate().
  • Retry safely. With the same clientRequestId, a retry answers with the jobs the first call started, marked deduped: true, and submits and charges nothing. Never retry a paid call without one. Every paid call on this page accepts it, frame() and voice() included.
  • The lane is chosen for you. For a clip, the video route is picked from the shot's inputs and returned as lane: generate-video or text-to-video.

productions.frame(productionId, input)

Takes a still from a shot's active clip and puts it where target says. It waits for the job, which takes seconds, and returns the changed production and the image url.

frame(productionId: string, input: {
  shotId: string
  mode?: "first" | "last" | "timestamp"
  timestamp?: number
  target?: "new-shot" | "start-frame" | "end-frame" | "still"
  clientRequestId?: string
}): Promise<StudioMediaResponse>

Prop

Type

const { url } = await client.studio.productions.frame(productionId, { shotId: "shot-2", mode: "last" })

productions.voice(productionId, input)

Speaks a shot's line and records it on the shot. It waits for the job.

voice(productionId: string, input: {
  shotId: string
  text: string
  voiceId?: string
  voiceType?: "premade" | "custom" | "library"
  ttsProvider?: string
  delivery?: Record<string, number>
  clientRequestId?: string
}): Promise<StudioMediaResponse>

Prop

Type

await client.studio.productions.voice(productionId, { shotId: "shot-3", text: "We're out of time." })

productions.revoice(productionId, input)

Recasts the voices of a shot's active clip. It takes minutes, so it returns a jobId, and the new clip lands through its marker.

revoice(productionId: string, input: { shotId: string; plan: Record<string, unknown>; clientRequestId?: string }): Promise<{ production: StudioProduction; jobId: string }>

Prop

Type

const { jobId } = await client.studio.productions.revoice(productionId, {
  shotId: "shot-3",
  plan: recastPlan, // the speaker-ordered plan the voice recast route takes
})

productions.music(productionId, input)

Scores the film. The finished track lands through its pending marker.

music(productionId: string, input: {
  prompt: string
  duration?: number
  instrumental?: boolean
  vocalGender?: string
  model?: string
  clientRequestId?: string
}): Promise<{ production: StudioProduction; jobId: string }>

Prop

Type

const { jobId } = await client.studio.productions.music(productionId, {
  prompt: "Tense strings building to a chase",
  instrumental: true,
})

Share and copy

share() opens the share-by-link view and unshare() closes it again. Sharing is its own call, never an operation, so who can see the work never changes as a side effect of an edit. clone() copies a production you own or can see into your own Studio project.

share(productionId: string): Promise<StudioProduction>
unshare(productionId: string): Promise<StudioProduction>
clone(productionId: string, input?: { name?: string }): Promise<StudioProduction>

Prop

Type

await client.studio.productions.share(productionId)
const copy = await client.studio.productions.clone(productionId, { name: "Rome chase, take 2" })

A copy starts private and visible: sharing and archiving never carry over. It is copied through your view of the source, so someone else's bin does not come with it.

client.studio: planned frames

These methods cover planned keyframes and their review. Check capabilities() before you offer a control, and call reconcile() once when you reopen a production, because a submission's answer may have been lost. Nothing here starts generation or accepts a candidate unless you call that method.

MethodWhat it does
capabilities()Read the plan versions and the operations this deployment supports
skill(), list(), validatePlan(), create()The same reads and create as the productions layer, in the envelope
get(id, options?)Read a production with its capabilities
edit(id, input)Apply operations with revision conditions
saveEditorState(id, input)Save ordinary editor fields against the loaded revision
generateKeyframe(id, input)Generate a planned frame, without accepting it
generateShot(id, input)Quote or submit a still or a clip
acceptKeyframe(id, review, concurrency?)Accept a reviewed candidate
reconcile(id)Record finished jobs, without accepting anything
setShared(id, input)Share or unshare, bound to a reviewed revision
clone(id, input?)Copy a saved production
importBundle(input)Import a portable production
appendBundle(id, input)Append a bundle to a production

studio.capabilities()

Returns the plan versions and which planned-frame operations this deployment supports.

capabilities(): Promise<{ data: StudioProductionCapabilities }>
const { data: caps } = await client.studio.capabilities()
if (caps.operations.generateKeyframes) showGenerateFrameButton()

operations has one flag per operation, such as readKeyframes, editKeyframes, generateKeyframes, acceptKeyframes, rejectKeyframes, editSequencePlans, generateLinkedClips, retakeLinkedClips, saveEditorState, revisionedSharing, editableSharedCopies, cloneLinkedProductions, importPlannedBundles, importLinkedBundles, appendPlannedBundles and appendLinkedBundles. automaticAcceptance and unattendedGeneration are always false.

studio.skill(), list(), validatePlan() and create()

The same calls as productions.skill(), list(), validatePlan() and create(), returned in the { data } envelope. In list(), the rows are in response.data.data.

skill(): Promise<{ data: Record<string, unknown> }>
list(options?: { limit?: number; cursor?: string; includeArchived?: boolean }): Promise<{ data: {
  data: Array<{ id: string; name: string; version: number; updatedAt: string; thumbnailUrl: string | null; shared: boolean; archived: boolean; shotCount: number }>
  nextCursor?: string
} }>
validatePlan(plan: Record<string, unknown>): Promise<{ data: { valid: boolean; errors: object[]; warnings: object[]; summary?: object } }>
create(input: { name?: string; plan?: Record<string, unknown> }): Promise<{ data: StudioProductionReply }>

Prop

Type

const { data: page } = await client.studio.list({ limit: 20 })
for (const row of page.data) console.log(row.name, row.shotCount)

studio.get(id, options?)

Reads a production with its capabilities. It never lands jobs.

get(id: string, options?: { detail?: "summary" | "full"; shotId?: string }): Promise<{ data: StudioProductionReply }>

Prop

Type

const { data: { production } } = await client.studio.get(productionId, { detail: "full" })
const frame = production.keyframes?.[0] // { id, label, revision, previewUrl, acceptedUrl, pending, ... }

studio.edit(id, input)

Applies operations with revision conditions (POST .../:id/ops), in the envelope. Use a strict baseVersion for remove_shot, restore_trashed and purge_trashed, and detach a bound sequence segment before you remove its scene.

edit(id: string, input: { ops: Array<{ op: string; [field: string]: unknown }>; baseVersion?: number; strict?: boolean; clientRequestId?: string }): Promise<{ data: StudioProductionReply & { version: number; rebased: boolean; receipts: object[] } }>

Prop

Type

await client.studio.edit(productionId, {
  ops: [{ op: "reject_keyframe_result", keyframeId, expectedRevision, resultKey, expectedAcceptedResultKey, reason: "Face drifted" }],
  baseVersion: version,
  strict: true,
})

A few planned-frame operations sent through edit():

  • reject_keyframe_result records Needs revision without generating. It needs operations.rejectKeyframes.
  • update_sequence_plan edits a sequence's ordered segments, each { shotId, startKeyframeId, endKeyframeId }, and keeps the scene ids. It needs operations.editSequencePlans.
  • detach_sequence_segment makes one segment independent, with mode set to clear or keep-accepted. It needs operations.editSequencePlans.
  • purge_trashed empties the bin entries you show, and clear_trash empties every bin, planned frames included.

studio.saveEditorState(id, input)

Saves ordinary editor fields against the revision you loaded. Check operations.saveEditorState first. The save is always strict: a conflict fails with a 409, so keep the local draft and reload before you resolve it.

saveEditorState(id: string, input: { expectedVersion: number; graph: object; clientRequestId?: string })

Prop

Type

await client.studio.saveEditorState(productionId, { expectedVersion: version, graph })

The save cannot change frame plans, acceptance, endpoint bindings, job history, protected bin entries or sharing. Use their own actions for those.

studio.generateKeyframe(id, input)

Generates a planned frame without accepting it. There is no dry run for frames.

generateKeyframe(id: string, input: { keyframeId: string; expectedRevision: number; clientRequestId?: string; overrides?: Record<string, unknown> }): Promise<{ data: { jobIds: string[]; deduped?: true; lane?: string } }>

Prop

Type

const { data: caps } = await client.studio.capabilities()
const { data: { production } } = await client.studio.get(productionId, { detail: "full" })
const frame = production.keyframes?.[0]

if (frame && caps.operations.generateKeyframes) {
  const { data: generation } = await client.studio.generateKeyframe(productionId, {
    keyframeId: frame.id,
    expectedRevision: frame.revision,
    clientRequestId: crypto.randomUUID(),
  })
  // follow generation.jobIds with client.jobs, then call reconcile()
}

Generating does not accept a candidate and does not create a character portrait. A cast reference that has only a description needs no portrait.

studio.generateShot(id, input)

Quotes or submits a still or a clip for a shot.

generateShot(id: string, input: StudioShotGenerationInput): Promise<{ data: StudioGenerationReply }>

Prop

Type

const { data: quote } = await client.studio.generateShot(productionId, { kind: "clip", shotId, dryRun: true })
if ("inputHash" in quote) {
  await client.studio.generateShot(productionId, {
    kind: "clip",
    shotId,
    expectedInputHash: quote.inputHash,
    clientRequestId: crypto.randomUUID(),
  })
}

Linked clips. A quote for a clip between planned frames includes inputHash, the accepted endpointPins, the normalized duration, resolution, aspect ratio and sound settings, and the creditIdentifier used for the price. Pass the reviewed inputHash as expectedInputHash. When the settings or the accepted frames changed since the quote, the call fails with 409 sequence_quote_changed before anything is submitted; ask for a new quote. The credits in a quote are an estimate: the generation reserves the current price.

Retakes. When operations.retakeLinkedClips is true, pass retakeResultKey with kind: "clip" and shotId, and quote it with dryRun: true. Submit with the reviewed expectedInputHash and a new clientRequestId, and leave out mode, overrides and count. A retake reuses the original request and the kept frame images, even after the plan or the acceptance changed. Earlier takes stay in the history. A take without a verifiable original request or kept images is refused, and a retake does not reproduce the same video bytes.

studio.acceptKeyframe(id, review, concurrency?)

Accepts a reviewed candidate for a planned frame. It is a separate, explicit step: generation never calls it.

acceptKeyframe(id: string, review: StudioKeyframeAcceptanceInput, concurrency?: { baseVersion?: number; strict?: boolean; clientRequestId?: string })

Prop

Type

await client.studio.acceptKeyframe(productionId, {
  keyframeId: frame.id,
  expectedRevision: frame.revision,
  resultKey,
  expectedAcceptedResultKey: frame.acceptedResultKey,
  requirementChecks, // one { requirementId, outcome } per requirement of the frame
})

A conflict throws the usual error. The SDK never picks another result or retries against a newer revision on its own.

studio.reconcile(id)

Records finished jobs, without accepting any candidate and without starting generation. It also checks jobs of scenes in the bin: a finished clip stays in that scene's stored graph, and you recover it with restore_trashed.

reconcile(id: string): Promise<{ data: StudioProductionReply & { landed: string[]; pending: string[]; failed: string[]; version: number } }>

Prop

Type

const { data } = await client.studio.reconcile(productionId)
console.log(data.landed, data.pending)

studio.setShared(id, input)

Shares or unshares a production, bound to the revision you reviewed. Check operations.revisionedSharing and pass expectedVersion: a concurrent edit then fails with 409 workflow_conflict, and the SDK does not retry. Only callers allowed to change visibility can use it.

setShared(id: string, input: { shared: boolean; allowEditableCopy?: boolean; expectedVersion?: number }): Promise<{ data: StudioProductionReply }>

Prop

Type

await client.studio.setShared(productionId, { shared: true, allowEditableCopy: true, expectedVersion: version })

With allowEditableCopy, an owner or workspace admin lets link viewers copy the saved plan, prompts, cast descriptions, kept reference inputs and take history. The bin and private review notes are never included. Copies start private, with no frames accepted. Turning copying off, or unsharing, blocks new copies; copies already made stay independent.

studio.clone(id, input?)

Copies a saved production. Check operations.cloneLinkedProductions before you copy one with linked frames, and pass its loaded expectedVersion: a changed source fails with a 409.

clone(id: string, input?: { name?: string; projectId?: string; expectedVersion?: number }): Promise<{ data: StudioProductionReply }>

Prop

Type

const { data: { production: copy } } = await client.studio.clone(productionId, {
  name: "Rome chase copy",
  expectedVersion: version,
})

The copy starts private. It keeps the frame inputs, which count against your storage, gets new frame, scene and sequence ids, and carries no running jobs and no accepted frames. Review and accept its frames before you generate media that depends on them. Copying submits no generation.

studio.importBundle(input)

Imports a portable production as a new, private production with new scene, frame and sequence ids (POST .../import-bundle). Check operations.importPlannedBundles for recipes and plans without media, and importLinkedBundles for bundles with kept frame media.

importBundle(input: { bundle: Record<string, unknown>; projectId?: string }): Promise<{ data: StudioProductionReply }>

Prop

Type

const { data: { production } } = await client.studio.importBundle({ bundle })

A linked bundle names its source production; the server checks that you own it and verifies every kept image before it copies anything. Missing access or forged provenance refuses the import before the new production is created. Neither kind of import carries acceptance or running jobs, and neither generates media.

studio.appendBundle(id, input)

Appends a complete bundle to an editable production, with new ids and an exact revision check (POST .../:id/import-bundle). An OAuth token needs workflows:write, and you need edit access to the production. Check appendPlannedBundles or appendLinkedBundles first.

appendBundle(id: string, input: { bundle: Record<string, unknown>; expectedVersion: number; afterShotId?: string; applyFilm?: boolean }): Promise<{
  data: { production: StudioProductionRecord; importedShotIds: string[]; importedKeyframeIds: string[] }
}>

Prop

Type

const { data } = await client.studio.appendBundle(productionId, { bundle, expectedVersion: version })
console.log(data.importedShotIds)

Existing scenes, frames, jobs, sharing and other settings stay; imported cast roles are merged. The imported frames need to be accepted again. An unknown afterShotId or a stale expectedVersion fails.

client.shots

Shot records behind /s/:id share links, for Share and Remix. A shot stores a builder's state: picker selections, prompts, target models, @ mention references and result URLs, under an unguessable 12-character id that is also the share key. Shots are private by default; sharing is a visibility change you make.

create(input?: CreateShotInput): Promise<{ id: string }>
get(id: string): Promise<{ shot: Shot }>
update(id: string, input: UpdateShotInput): Promise<{ shot: Shot }>
delete(id: string): Promise<void>

Prop

Type

const { id } = await client.shots.create({ mode: "single", freeText: "A lighthouse in a storm", models: ["nano-banana-2"] })
await client.shots.update(id, { visibility: "public" }) // anyone with the id can now read it
const { shot } = await client.shots.get(id)

A public shot is readable by anyone with its id. A private shot is readable only by its owner, and others get NotFoundError. Only the owner can update or delete a shot.

Frequently asked questions

Last updated on

On this page