Quickstart: TypeScript
Prepare and confirm a video from TypeScript with the Probably SDK, or with fetch against Probably's HTTP API.
This quickstart calls the HTTP API from TypeScript two ways: with the Probably SDK, a typed client with one method per tool, and with plain fetch, which needs nothing installed beyond a parser.
With the SDK
@probablyco/sdk is generated from the API's OpenAPI document, so every tool's input and answer is typed exactly as the HTTP API reference lists it. It runs on servers (Node 20 or later, Bun, Deno) with your API key; the API refuses keys sent from a web page.
npm install @probablyco/sdk
export PROBABLY_API_URL="https://studio.probably.co"
export PROBABLY_API_KEY="prb_..."This script lists the Spaces the key reaches and prepares a video in the first one. Preparing spends nothing: it answers with a preview and a confirmation.
// List your Spaces and prepare a video with the TypeScript SDK. Preparing
// spends nothing: it answers with a preview and a confirmation, which you
// confirm with confirm_spend once the person agrees (see quickstart.ts).
// Run it with Bun, Deno or Node 23.6+:
// npm install @probablyco/sdk
// PROBABLY_API_KEY=prb_... node sdk-quickstart.ts
import { Probably, ProbablyError } from "@probablyco/sdk"
const apiKey = process.env.PROBABLY_API_KEY
if (!apiKey) throw new Error("Set PROBABLY_API_KEY to a key from Settings → Connections")
// The production API, unless PROBABLY_API_URL names another.
const probably = new Probably({ apiKey, baseUrl: process.env.PROBABLY_API_URL })
try {
const { spaces } = await probably.tools.list_spaces()
console.log("Spaces:", spaces.map((space) => space.name).join(", "))
const [space] = spaces
if (!space) throw new Error("This key reaches no Space")
// A new request key per video: retrying with the same key returns the same draft.
const draft = await probably.tools.prepare_video({
spaceId: space.id,
brief: "A 15-second TikTok for a reef-safe SPF 50 sunscreen, filmed on a hiking trail",
lengthSeconds: 15,
requestKey: crypto.randomUUID(),
})
console.log(`Prepared ${draft.videoId}, nothing spent yet:`, draft.preview)
console.log(`Confirm ${draft.confirmationId} before ${draft.expiresAt} to start it.`)
} catch (error) {
// A refusal names its outcome, such as "rate-limited" (see /docs/errors).
if (!(error instanceof ProbablyError)) throw error
console.error(
`Refused: ${error.outcome} (${error.status}), request ${error.requestId}`,
error.message,
)
process.exitCode = 1
}Each tool is a method, probably.tools.<name>(input), and probably.events.list({ after, types, limit }) reads events by cursor. Every answer is checked against the tool's schema. A refusal throws a ProbablyError whose outcome names it (see Errors), with the HTTP status, the requestId and, when the API says, retryAfterSeconds; an answer the SDK can't read throws one with outcome invalid-response. A call gives up after 90 seconds, or the timeoutMs you give the client. To start the work, show the person draft.preview and, once they agree, call probably.tools.confirm_spend({ confirmationId: draft.confirmationId }), as the next script does.
With fetch
The script below prepares a video from a brief, confirms the spend once you agree to its preview, and reads the video back, with fetch alone.
Set up
Create a key in Settings → Connections (see Authentication). For this script to confirm the spend itself, give the key Make changes and Start paid work it prepares, and limit it to one Space with a spend cap. Without the spend permission, the script prints a link where you confirm the work in the studio instead.
export PROBABLY_API_URL="https://studio.probably.co"
export PROBABLY_API_KEY="prb_..."
bun add zodThe script runs with Bun, or with Node 23.6 or later, which run TypeScript directly. It uses zod to check the fields it reads from each answer.
The script
// Prepare a video from a brief, confirm the spend once you agree to its
// preview, and read the video back. Run it with Bun or Node 23.6+, with zod
// installed:
// PROBABLY_API_KEY=prb_... bun quickstart.ts
// It asks before it spends; pass --yes to agree in advance, as a script you
// have already approved might.
import { createInterface } from "node:readline/promises"
import { z } from "zod"
const apiUrl = process.env.PROBABLY_API_URL ?? "https://studio.probably.co"
const apiKey = process.env.PROBABLY_API_KEY
if (!apiKey) throw new Error("Set PROBABLY_API_KEY to a key from Settings → Connections")
/** A JSON value. */
type Json = string | number | boolean | null | Json[] | { [key: string]: Json }
/** A tool's input: the JSON object its schema describes. */
type ToolInput = { [field: string]: Json }
/** Calls one tool with its input; its answer, checked by `answer`, or an error with the refusal. */
async function call<Answer>(tool: string, input: ToolInput, answer: z.ZodType<Answer>) {
const response = await fetch(`${apiUrl}/api/v1/tools/${tool}`, {
method: "POST",
headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
body: JSON.stringify(input),
})
// A refusal names its outcome, such as "not-enough-credits" (see /docs/errors).
if (!response.ok) throw new Error(`${tool}: ${response.status} ${await response.text()}`)
return answer.parse(await response.json())
}
// The fields this script reads; the API reference lists every one.
const spaceList = z.object({ spaces: z.array(z.object({ id: z.string(), name: z.string() })) })
const prepared = z.object({
videoId: z.string(),
confirmationId: z.string(),
expiresAt: z.string(),
preview: z.looseObject({ kind: z.string() }),
confirmUrl: z.string().nullable(),
})
const confirmed = z.object({ videoId: z.string(), result: z.looseObject({}) })
const video = z.object({
name: z.string(),
run: z.object({ status: z.string() }).nullable(),
})
const { spaces } = await call("list_spaces", {}, spaceList)
const [space] = spaces
if (!space) throw new Error("This key reaches no Space")
// A new request key per video: retrying with the same key returns the same draft.
const draft = await call(
"prepare_video",
{
spaceId: space.id,
brief: "A 15-second TikTok for a reef-safe SPF 50 sunscreen, filmed on a hiking trail",
lengthSeconds: 15,
requestKey: crypto.randomUUID(),
},
prepared,
)
console.log(`Prepared a video in ${space.name}:`, draft.preview)
/** Whether the person running this agrees to spend: `--yes` given in advance, or a yes typed now. */
async function agreed(question: string) {
if (process.argv.includes("--yes")) return true
const prompt = createInterface({ input: process.stdin, output: process.stdout })
const answer = await prompt.question(`${question} [y/N] `)
prompt.close()
return /^y(es)?$/i.test(answer.trim())
}
// Nothing is spent until the person agrees to the preview above. A key that
// may not spend gets a link for them to confirm it in the studio instead.
if (draft.confirmUrl) {
console.log(`Confirm it in the studio: ${draft.confirmUrl}`)
} else if (await agreed("Start this video, spending what the preview shows?")) {
const started = await call("confirm_spend", { confirmationId: draft.confirmationId }, confirmed)
console.log("Started:", started.result)
} else {
console.log(`Not started. The confirmation expires at ${draft.expiresAt}.`)
}
const current = await call("get_video", { videoId: draft.videoId }, video)
console.log(current.run ? `${current.name}: ${current.run.status}` : `${current.name}: not started`)What it does
Finds a Space
list_spaces answers with the Spaces the key reaches; a key limited to one Space sees only that
one.
Prepares the video
prepare_video creates the video as a draft at the length you give it, 15, 30, 45 or 60
seconds, and answers with a preview of what its first run costs and a confirmationId. Nothing
is spent yet. The requestKey makes the call safe to retry: the same key answers with the same
draft.
Confirms the spend
The script asks you before it spends, and calls confirm_spend only on a yes (or with --yes,
when you've agreed in advance). confirm_spend starts the run, exactly as previewed. Confirming
the same ID again starts nothing more and answers with what it started. In your own product,
show the person the preview and confirm only once they agree.
Reads the video
get_video answers with the video's state. Call wait_for_video to wait for its next change,
or read events by cursor.