TypeScript SDK for Simmit, an API for running SimulationCraft in the cloud.
Send the same .simc profile you'd run locally and Simmit executes SimulationCraft (SimC) for you on managed hardware, so you can run sims from anywhere that can make an HTTP request, with no local SimC build to manage.
Documentation: dashboard.simmit.com/docs
npm install @simmit/sdkNode 20+. Zero runtime dependencies (global fetch and WebCrypto only).
A Simmit job is one SimC run: you submit a profile, Simmit queues and runs it in the cloud, and you fetch the result when it's done.
import Simmit from '@simmit/sdk'
const client = new Simmit({
secretKey: process.env['SIMMIT_SECRET_KEY'] // This is the default and can be omitted
})
// Submit a SimC profile, wait for the sim to finish, and read the result.
const job = await client.jobs.createAndWait({
build: { channel: 'latest' },
profile: { text: profileText } // a SimC profile, up to 2 MB
})
const result = await client.jobs.getResult(job.id)The rest of the surface is shown below until an SDK reference lands in the docs.
createAndWait is ideal for scripts and queue workers that can hold a promise
open. Hook into progress, or capture the job id before polling starts:
await client.jobs.createAndWait(
{ build: { channel: 'latest' }, profile: { text: profileText } },
{
onCreated: (res) => console.log('queued', res.id),
onPoll: (status) => console.log(status.status, status.progress.percent)
}
)For web apps that can't hold a promise open, drive the lifecycle yourself:
create returns immediately with a job id to persist, then poll getStatus on
your own cadence and read the result once terminal. A job.terminal webhook is
the reliable completion signal when a browser poll can pause (see below).
const { id } = await client.jobs.create({
build: { channel: 'nightly' },
profile: { text: profileText }
})
// persist `id`, return to the caller, then from a later request:
const status = await client.jobs.getStatus(id) // status + progress, in any state
if (status.status === 'completed') {
const result = await client.jobs.getResult(id)
}
await client.jobs.cancel(id) // request cancellationgetResult returns each artifact with a stable download url, valid for the
artifact's full retention window. To fetch that URL on demand instead (e.g. a
browser flow that controls the final fetch), use:
const { url } = await client.artifacts.getUrl(artifactId)const balance = await client.credits.get()const usage = await client.usage.get()
usage.limits.maxActiveJobs // the key's concurrency ceiling (number | null)
usage.snapshot.activeJobs // jobs in flight right now (number | null)Every API error is a typed subclass of SimmitError, with a narrowed code
and meta:
import { InsufficientCreditsError, InvalidProfileError } from '@simmit/sdk'
try {
await client.jobs.create({ build: { channel: 'latest' }, profile: { text } })
} catch (err) {
if (err instanceof InvalidProfileError) {
console.error(err.meta.blocked) // the rejected profile lines
} else if (err instanceof InsufficientCreditsError) {
console.error(err.meta?.maxAffordableRuntimeSeconds)
} else {
throw err
}
}createAndWait also throws JobFailedError / JobCancelledError /
JobTimedOutError (each carrying the full .job), and JobWaitTimeoutError if
the wait deadline passes before the job finishes. The job keeps running, so call
client.jobs.cancel(jobId) to stop the spend.
Single-request methods return an APIPromise. Await it for the parsed body, or
use .withResponse() to reach the raw Response (rate-limit headers, idempotent
replays):
const { data, response } = await client.jobs.create(params).withResponse()
response.headers.get('x-idempotent-replay')unwrapWebhook verifies a webhook signature and returns the parsed event. It is
standalone (no client and no API key required, just your webhook signing
secret), so it is safe to run in a webhook receiver:
import { unwrapWebhook } from '@simmit/sdk'
const event = await unwrapWebhook(
rawBody, // the raw request body, exactly as received
signatureHeader, // the X-Simmit-Signature header value
process.env.SIMMIT_WEBHOOK_SECRET
)
if (event.payload.status === 'completed') {
// ...
}The SDK is server-side only: the secret key spends credits and must never reach
the browser. A typical web flow submits a job on one request, persists the
returned id, and reads the result on a later request. A job.terminal webhook
(above) is the reliable completion signal.
new Simmit() throws when no secret key is set. Frameworks that evaluate route
modules at build time (Next.js, Remix, SvelteKit) run that code with no
environment, so a top-level new Simmit() breaks the build. Construct it lazily
so the key is read at first use, on a real request:
// lib/simmit.ts
import 'server-only'
import Simmit from '@simmit/sdk'
let client: Simmit | undefined
export function simmit(): Simmit {
return (client ??= new Simmit())
}The server-only import fails the build if this module is ever pulled into a
client component.
jobs.create attaches a fresh idempotency key per call, which keeps the SDK's
own retries safe. It does not cover a user double-clicking submit: that is two
calls, and two billed jobs. Pass a stable key (for example a per-render form
nonce) so the second submit replays the first job instead of creating another:
await simmit().jobs.create(params, { idempotencyKey: formNonce })Use isTerminal to tell whether the job is still running. A plain
job.status === 'completed' check narrows a finished job to CompletedJob
before you read its result:
import { isTerminal } from '@simmit/sdk'
import { simmit } from '@/lib/simmit'
const job = await simmit().jobs.get(id)
if (!isTerminal(job.status)) {
// still running: send the caller back to a progress view
return
}
if (job.status !== 'completed') {
// terminal but not successful: 'failed' | 'cancelled' | 'timed_out'
console.error(job.statusReason)
return
}
// job is CompletedJob here (=== 'completed' narrowed it)
const { result } = await simmit().jobs.getResult(job.id)
const actor = result.summary?.mainActor
// mainActor is null for a completed run with no single headline actor (a
// profileset-only or multi-player sim keeps per-actor numbers in the JSON
// artifact), so guard it even on success.
const dps = actor ? Math.round(actor.mean) : null- Node 20+ (
.nvmrcpins the dev version), pnpm. pnpm generateregeneratessrc/generated/openapi.d.tsfrom the committedopenapi.jsonsnapshot. Never hand-edit generated output; onlysrc/api-types.tsmay import fromsrc/generated/.pnpm buildbuilds dual ESM+CJS via tsup.pnpm testruns vitest (hermetic; mocksfetch, no network).pnpm smokeruns a manual check against a real API (needsSIMMIT_SECRET_KEY; setSIMMIT_PROFILE_FILEto also run a full create-then-result, orTEST_API_BASE_URLto target a non-prod endpoint). Seescripts/smoke.mjs.
MIT