> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sigmora.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Client libraries

> A complete TypeScript and Python client for the Sigmora API, and how to generate a typed client from the OpenAPI document.

The Sigmora API is JSON over HTTPS, so any HTTP client works. This page gives you
two small, complete clients you can paste into a project — they handle
authentication, idempotency keys, retries, job polling and the error envelope —
and shows how to generate a fully typed client from the
[OpenAPI document](https://api.sigmora.org/v1/openapi.json).

Both clients follow the same rules:

* Read the key from `SIGMORA_API_KEY` and send it as `Authorization: Bearer ...`.
* Send an `Idempotency-Key` on every `POST`, and reuse it when retrying.
* Retry only `rate_limited` and `upstream_unavailable`, waiting for `Retry-After`
  when it is present.
* Raise an error carrying `code`, `error` (the message) and the `x-request-id`.
* Poll jobs until `status` is `done`, `cancelled`, `failed` or `qa_failed`, and
  stop at `awaiting_review`, which needs a person.

## TypeScript

Requires Node.js 18 or later (for the built-in `fetch`).

```typescript sigmora.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { randomUUID } from "node:crypto";

const RETRYABLE = new Set(["rate_limited", "upstream_unavailable"]);
const TERMINAL = new Set(["done", "cancelled", "failed", "qa_failed"]);

export class SigmoraError extends Error {
  constructor(
    readonly status: number,
    readonly code: string,
    message: string,
    readonly details?: unknown,
    readonly requestId?: string | null,
  ) {
    super(`${code}: ${message}`);
  }
}

export type Job = {
  id: string;
  kind: string;
  status: "queued" | "rendering" | "awaiting_review" | "done" | "cancelled" | "failed" | "qa_failed";
  phase?: string | null;
  error?: string | null;
  artifacts?: Record<string, unknown> | null;
};

type RequestOptions = {
  body?: unknown;
  query?: Record<string, string | number | boolean | undefined>;
  idempotencyKey?: string;
  timeoutMs?: number;
};

export class Sigmora {
  constructor(
    private readonly apiKey = process.env.SIGMORA_API_KEY ?? "",
    private readonly baseUrl = "https://api.sigmora.org",
    private readonly maxRetries = 4,
  ) {
    if (!apiKey) throw new Error("Set SIGMORA_API_KEY");
  }

  async request<T = any>(method: string, path: string, options: RequestOptions = {}): Promise<T> {
    const url = new URL(path, this.baseUrl);
    for (const [key, value] of Object.entries(options.query ?? {})) {
      if (value !== undefined) url.searchParams.set(key, String(value));
    }
    // A POST is only safe to retry when it carries an idempotency key.
    const idempotencyKey = method === "GET" ? undefined : (options.idempotencyKey ?? randomUUID());

    for (let attempt = 0; ; attempt++) {
      const response = await fetch(url, {
        method,
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          "Content-Type": "application/json",
          ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
        },
        body: options.body === undefined ? undefined : JSON.stringify(options.body),
        signal: AbortSignal.timeout(options.timeoutMs ?? 120_000),
      });
      const body = await response.json().catch(() => ({}));
      if (response.ok) return body as T;

      const code: string = body.code ?? "internal_error";
      if (RETRYABLE.has(code) && attempt < this.maxRetries) {
        const retryAfter = Number(response.headers.get("Retry-After"));
        const waitMs = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 1000 + Math.random() * 250;
        await new Promise((resolve) => setTimeout(resolve, waitMs));
        continue;
      }
      throw new SigmoraError(
        response.status,
        code,
        body.error ?? response.statusText,
        body.details,
        response.headers.get("x-request-id"),
      );
    }
  }

  get<T = any>(path: string, query?: RequestOptions["query"]) {
    return this.request<T>("GET", path, { query });
  }

  post<T = any>(path: string, body: unknown, idempotencyKey?: string, timeoutMs?: number) {
    return this.request<T>("POST", path, { body, idempotencyKey, timeoutMs });
  }

  /** Poll a job until it finishes or needs a person. */
  async waitForJob(jobId: string, { intervalMs = 15_000, timeoutMs = 60 * 60_000 } = {}): Promise<Job> {
    const deadline = Date.now() + timeoutMs;
    while (true) {
      const { job } = await this.get<{ job: Job }>(`/v1/jobs/${encodeURIComponent(jobId)}`);
      if (TERMINAL.has(job.status) || job.status === "awaiting_review") return job;
      if (Date.now() > deadline) throw new Error(`Job ${jobId} still ${job.status} after ${timeoutMs} ms`);
      await new Promise((resolve) => setTimeout(resolve, intervalMs + Math.random() * 2_000));
    }
  }
}
```

Use it to create a project, render a video into it and collect the file:

```typescript main.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { Sigmora } from "./sigmora";

const sigmora = new Sigmora();

// Persist idempotency keys with your own record of the work so a restart retries
// the same logical call instead of starting (and paying for) a new one.
const { project } = await sigmora.post(
  "/v1/projects",
  { title: "Statistics for AI", mediaType: "video", videoPath: "general" },
  "project-stats-week5",
);

const render = await sigmora.post(
  "/v1/video/render",
  {
    projectId: project.id,
    story: "Statistics is the science of collecting, organising and interpreting data...",
    videoType: "lesson",
  },
  "render-stats-week5",
);

const job = await sigmora.waitForJob(render.jobId);
if (job.status === "awaiting_review") throw new Error("A person must approve this edit first");
if (job.status !== "done") throw new Error(job.error ?? `Render ended as ${job.status}`);
console.log(job.artifacts?.download);
```

## Python

Requires Python 3.9 or later and `requests` (`pip install requests`).

```python sigmora.py theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import os
import random
import time
import uuid

import requests

RETRYABLE = {"rate_limited", "upstream_unavailable"}
TERMINAL = {"done", "cancelled", "failed", "qa_failed"}


class SigmoraError(Exception):
    def __init__(self, status, code, message, details=None, request_id=None):
        super().__init__(f"{code}: {message}")
        self.status = status
        self.code = code
        self.message = message
        self.details = details
        self.request_id = request_id


class Sigmora:
    def __init__(self, api_key=None, base_url="https://api.sigmora.org", max_retries=4):
        self.api_key = api_key or os.environ["SIGMORA_API_KEY"]
        self.base_url = base_url
        self.max_retries = max_retries
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {self.api_key}"

    def request(self, method, path, *, json=None, params=None, idempotency_key=None, timeout=120):
        headers = {}
        if method != "GET":
            # A POST is only safe to retry when it carries an idempotency key.
            headers["Idempotency-Key"] = idempotency_key or str(uuid.uuid4())

        for attempt in range(self.max_retries + 1):
            response = self.session.request(
                method, self.base_url + path, json=json, params=params, headers=headers, timeout=timeout
            )
            try:
                body = response.json()
            except ValueError:
                body = {}
            if response.ok:
                return body

            code = body.get("code", "internal_error")
            if code in RETRYABLE and attempt < self.max_retries:
                retry_after = response.headers.get("Retry-After")
                wait = float(retry_after) if retry_after else 2**attempt + random.random() / 4
                time.sleep(wait)
                continue
            raise SigmoraError(
                response.status_code,
                code,
                body.get("error", response.reason),
                body.get("details"),
                response.headers.get("x-request-id"),
            )

    def get(self, path, **params):
        return self.request("GET", path, params=params or None)

    def post(self, path, body, idempotency_key=None, timeout=120):
        return self.request("POST", path, json=body, idempotency_key=idempotency_key, timeout=timeout)

    def wait_for_job(self, job_id, interval=15, timeout=3600):
        """Poll a job until it finishes or needs a person."""
        deadline = time.monotonic() + timeout
        while True:
            job = self.get(f"/v1/jobs/{job_id}")["job"]
            if job["status"] in TERMINAL or job["status"] == "awaiting_review":
                return job
            if time.monotonic() > deadline:
                raise TimeoutError(f"Job {job_id} still {job['status']} after {timeout}s")
            time.sleep(interval + random.random() * 2)
```

Use it to create a project, render a video into it and collect the file:

```python main.py theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
from sigmora import Sigmora

sigmora = Sigmora()

# Persist idempotency keys with your own record of the work so a restart retries
# the same logical call instead of starting (and paying for) a new one.
project = sigmora.post(
    "/v1/projects",
    {"title": "Statistics for AI", "mediaType": "video", "videoPath": "general"},
    idempotency_key="project-stats-week5",
)["project"]

render = sigmora.post(
    "/v1/video/render",
    {
        "projectId": project["id"],
        "story": "Statistics is the science of collecting, organising and interpreting data...",
        "videoType": "lesson",
    },
    idempotency_key="render-stats-week5",
)

job = sigmora.wait_for_job(render["jobId"])
if job["status"] == "awaiting_review":
    raise RuntimeError("A person must approve this edit first")
if job["status"] != "done":
    raise RuntimeError(job.get("error") or f"Render ended as {job['status']}")
print(job["artifacts"]["download"])
```

## Generate a typed client

The OpenAPI 3.1 document at
[`https://api.sigmora.org/v1/openapi.json`](https://api.sigmora.org/v1/openapi.json)
describes every operation, with a stable `operationId` on each and named
component schemas for the objects you work with — `Job`, `JobArtifacts`,
`Project`, `Brand`, `Product`, `Campaign`, `Capability`, `Theme`, `Trend` and
more. Generators turn those into types and methods.

<Tabs>
  <Tab title="TypeScript">
    Generate types with [openapi-typescript](https://openapi-ts.dev) and call the
    API through the matching `openapi-fetch` client:

    ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    npm install openapi-fetch
    npx openapi-typescript https://api.sigmora.org/v1/openapi.json -o sigmora-api.d.ts
    ```

    ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    import createClient from "openapi-fetch";
    import type { components, paths } from "./sigmora-api";

    type Job = components["schemas"]["Job"];

    const client = createClient<paths>({
      baseUrl: "https://api.sigmora.org",
      headers: { Authorization: `Bearer ${process.env.SIGMORA_API_KEY}` },
    });

    const { data, error } = await client.GET("/v1/jobs/{jobId}", {
      params: { path: { jobId: "job_4k1b7d" } },
    });
    if (error) throw new Error(`${error.code}: ${error.error}`);
    const job: Job | undefined = data.job;
    ```
  </Tab>

  <Tab title="Python">
    Generate a package with [openapi-python-client](https://github.com/openapi-generators/openapi-python-client):

    ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    pipx run openapi-python-client generate \
      --url https://api.sigmora.org/v1/openapi.json \
      --output-path sigmora_api
    ```

    The generated package exposes one function per operation, named from its
    `operationId`, and a model class per component schema.
  </Tab>

  <Tab title="Other languages">
    Any OpenAPI 3.1 generator works, for example
    [OpenAPI Generator](https://openapi-generator.tech):

    ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    npx @openapitools/openapi-generator-cli generate \
      -i https://api.sigmora.org/v1/openapi.json \
      -g go \
      -o ./sigmora-go
    ```
  </Tab>
</Tabs>

<Tip>
  Pin the document you generate from. Download `openapi.json` into your
  repository, generate from that copy, and review the diff when you update it —
  see [Versioning](/versioning) for what can change within `v1`.
</Tip>

A generated client gives you types, not behavior. Keep the rules above —
idempotency keys, retrying only retryable codes, and stopping polls at terminal
statuses — in a thin wrapper around it.

## Next steps

<CardGroup cols={2}>
  <Card title="Errors" icon="triangle-exclamation" href="/errors">
    Every error code and how to respond.
  </Card>

  <Card title="Jobs & polling" icon="clock" href="/jobs">
    Statuses, artifacts and cancelling work.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.