npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fotohub

v1.10.0

Published

Official TypeScript SDK for the FOTOhub AI Platform — image, video, music, 3D generation, chat/LLM, translation, and storage.

Readme


Highlights

  • Zero dependencies --- uses native fetch (Node.js 18+, Deno, Bun, browsers)
  • Full TypeScript --- every request and response is strictly typed
  • ESM + CommonJS --- dual-package exports, works everywhere
  • Virtual try-on --- one garment, or a chained top + bottom outfit in a single call
  • IDA Q 1.0 --- FOTOhub's own image model, submitted and polled for you
  • Stability tools --- upscale, erase, inpaint, outpaint, recolor, style transfer
  • Billing in USD --- balance, top-ups, transactions, invoices, overage caps
  • Automatic retries --- exponential backoff on transient failures
  • Typed errors --- 12 distinct error classes for precise handling
  • Tiny footprint --- tree-shakeable, no bloat

Installation

npm install fotohub
pnpm add fotohub
yarn add fotohub
bun add fotohub

Requires Node.js 18+ (or any runtime with a global fetch implementation).


Quick Start

import { FotoHub } from "fotohub";

const client = new FotoHub({
  apiKey: process.env.FOTOHUB_API_KEY!,
});

// Generate an image
const image = await client.generateImage({
  prompt: "A cyberpunk cityscape at sunset, neon lights reflecting on wet streets",
  aspect_ratio: "16:9",
});

console.log(image.images[0]);

Options are snake_case. They are forwarded to the API as-is, so it is aspect_ratio, num_images, image_url — not aspectRatio. result.images is a string[] of URLs, not a list of objects.


Authentication

All authenticated endpoints require an API key. Get yours at fotohub.app/settings/api.

const client = new FotoHub({
  apiKey: "fh_live_...", // Your FOTOhub API key
});

The API key is sent as a Bearer token in the Authorization header. Four methods skip it — translate(), gabrielSuggest(), gabrielRecommend() and getTierCatalog(). Everything else, gabrielClassify() included, needs a key.

Security: Never expose your API key in client-side code. Use environment variables or a server-side proxy.


Usage Examples

Image Generation

Generate images from text prompts with 25+ AI models.

const result = await client.generateImage({
  prompt: "A serene Japanese garden in autumn, watercolor style",
  model: "seedream-5-0-260128",
  aspect_ratio: "16:9",
  num_images: 2,
  output_format: "png",
});

for (const url of result.images) {
  console.log(url);
}

console.log(result.credits_used);
console.log(result.billing.usd_charged);
console.log(result.metadata?.generation_time_ms);

guidance_scale and steps are accepted by the type but forwarded to an endpoint that ignores them — treat them as no-ops rather than knobs.

Image-to-Image (Style Reference)

const result = await client.generateImage({
  prompt: "Same scene but in winter with snow",
  reference_image_url: "https://example.com/original.jpg",
  reference_strength: 0.7,
});

IDA Q 1.0

FOTOhub's own model runs on a single-GPU queue, so it has a dedicated method that submits the job and polls until it finishes — ~30 s at 1K, up to ~3.5 min at 2K:

const result = await client.generateIdaQ({
  prompt: "Portret kobiety w świetle porannym",   // any language
  aspect_ratio: "4:3",
  image_size: "1.5K",
});

console.log(result.images[0]);

Video Generation

Video generation is synchronous — the call resolves once the video is ready and the result already contains video_url.

const video = await client.generateVideo({
  prompt: "A drone flying over a misty forest at sunrise",
  duration: 5,
  aspect_ratio: "16:9",
  resolution: "1080p",
});

console.log(video.video_url);    // Final video URL
console.log(video.duration);     // Duration in seconds
console.log(video.credits_used); // Credits consumed

Seedance — long clips and video editing

Seedance is the one video family that runs asynchronously: the API answers 202 with a job_id rather than a finished video, so generateVideo() cannot consume it. Use generateSeedance(), which submits the job and polls until it finishes.

seedance-2-5 renders 4–30 seconds in a single clip — the longest we offer — and its audio track is included in the price. The trade-off is resolution: 480p and 720p only. For 1080p or 4K stay on seedance-2-0-pro (4–15s).

const result = await client.generateSeedance({
  prompt: "A lone hiker crossing a snowfield, wind picking up, wide drone shot",
  duration: 30,
  resolution: "720p",
  generate_audio: true,          // free
  onProgress: (job) => console.log(`${job.status} ${job.progress ?? 0}%`),
});

console.log(result.video_url);
console.log(result.credits_used); // ~435 for 30s @ 720p

Rates are per second: 14.5 credits/s at 720p, 6.4 at 480p. Draft at 480p (a 5s test costs 32 credits) and re-render the take you like at 720p.

Edit or extend an existing video — pass reference_videos and duration: -1 to keep the source length:

const edited = await client.generateSeedance({
  prompt: "Make it golden hour, warmer light on the subject's face",
  reference_videos: ["https://example.com/clip.mp4"],
  duration: -1,
});

A video reference bills at the higher 17.6 credits/s (720p) because the source frames are charged as input.

Face consistency — register a portrait once, then reuse the asset id:

const asset = await client.registerVideoAsset("https://example.com/face.jpg");

const result = await client.generateSeedance({
  prompt: "The same woman walking through a night market, neon reflections",
  asset_ids: [asset.asset_id],
  duration: 10,
});

generateSeedance() throws JobTimeoutError if the job is still running when maxWait (default 30 min) expires, and JobFailedError if the render fails.


Music Generation

Generate original music from text descriptions.

const music = await client.generateMusic({
  prompt: "Upbeat electronic dance track, euphoric synths, festival energy",
  duration: 30,
  genre: "electronic",
  tempo: 128,
  instrumental: true,
});

console.log(music.audio_url);
console.log(music.duration);
console.log(music.credits_used);

Music costs 5 credits up to 30 s, 10 up to 60 s, 25 beyond that; duration is capped at 300 s. generateSfx() and generateSpeech() follow the same shape and also return audio_url.


Virtual Try-On

A try-on is a job, not a blocking call: a render takes about 11 seconds, so you submit and then wait.

const job = await client.tryOn({
  personImageUrl: "https://example.com/person.jpg",
  garmentImageUrl: "https://example.com/shirt.png",
  category: "tops",              // "tops" | "bottoms" | "one-pieces"
  garmentPhotoType: "flat-lay",
});

const result = await client.waitForTryOn(job.job_id, {
  onProgress: (r) => console.log(r.status, r.progress),
});
console.log(result.images?.[0]);

Pass garments to dress a top and a bottom in one job. The API applies the top first, feeds that render into the second pass, and charges 3 credits instead of 4:

const job = await client.tryOn({
  personImageUrl: "https://example.com/person.jpg",
  garments: [
    { garmentImageUrl: "https://example.com/tee.png", category: "tops" },
    { garmentId: "0f1e2d3c-...", category: "bottoms" },   // or a catalogue id
  ],
});

const result = await client.waitForTryOn(job.job_id, { maxWait: 60_000 });

// If the second pass failed, the top-only render still comes back and one credit
// is refunded — so check before calling it a finished outfit.
const partial = result.metadata?.partial_failure;
if (partial) {
  console.warn(`The ${partial.slot} is missing`, result.images?.[0]);
}

Exactly one top plus one bottom is required — two tops, three garments, or a one-pieces in the array are rejected with 400. Hats and shoes are not supported by the model at all.


Chat / LLM Completions

Non-Streaming

const response = await client.chat({
  messages: [
    { role: "user", content: "Explain quantum entanglement in simple terms" },
  ],
  model: "gpt-4o",
  max_tokens: 1000,
  temperature: 0.7,
});

console.log(response.choices[0].message.content);
console.log(response.credits_used);

chat() accepts exactly four model IDs — gemini-flash (default), gemini-pro, gpt-4o, claude-sonnet. Anything else is rejected with 400. For a full Claude model ID use chatClaude() or chatBedrock().

credits_used is the authoritative charge. usage is passed straight through from the provider and can arrive as {}, so read usage.total_tokens defensively.

Streaming

client.chatStream() throws ValidationError. It targeted /v1/ai/chat/completions, which accepts stream: true for OpenAI compatibility and then ignores it, returning one complete JSON body. The SSE parser found no data: frames there, so the iterator completed after zero chunks without throwing — an empty result for a request you were still billed for. It now throws before sending, which keeps the call free.

The one streaming endpoint is POST /v1/ai/agent/stream. There is no wrapper for it yet — call it with fetch. Frames are keyed by type and the stream ends at data: [DONE]:

interface AgentFrame {
  type: "text_delta" | "tool_use" | "done" | "error";
  text?: string;
  name?: string;
  usage?: { input_tokens: number; output_tokens: number; total_tokens: number };
  message?: string;
}

async function* agentFrames(prompt: string): AsyncGenerator<AgentFrame> {
  const res = await fetch("https://apis.fotohub.app/v1/ai/agent/stream", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.FOTOHUB_API_KEY}`,
    },
    body: JSON.stringify({
      model: "claude-sonnet-4.6",
      messages: [{ role: "user", content: prompt }],
    }),
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);

  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  outer: while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    // Frames are delimited by a blank line, not by a chunk boundary.
    const parts = buffer.split("\n\n");
    buffer = parts.pop() ?? "";
    for (const part of parts) {
      const line = part.split("\n").find((l) => l.startsWith("data: "));
      if (!line) continue;
      const data = line.slice(6);
      if (data === "[DONE]") break outer;   // the only reliable terminator
      yield JSON.parse(data) as AgentFrame;
    }
  }
}

for await (const frame of agentFrames("Write a short story about a robot")) {
  if (frame.type === "text_delta") process.stdout.write(frame.text ?? "");
  else if (frame.type === "error") throw new Error(frame.message);
}

Two things to know: the done frame is optional (omitted when the turn produced no tokens, replaced by error when generation succeeded but settlement failed), and abandoning the stream still bills you — the server settles the tokens it already generated.


Gabriel --- Model Routing

Gabriel returns a routing decision, not a completion: it names the feature and model to use, and you make that call yourself. An API key is required.

const decision = await client.gabrielClassify({
  prompt: "I want to create a professional headshot for LinkedIn",
  language: "en",
});

console.log(decision.action);          // route | answer | workflow | error
console.log(decision.target);          // the feature to send the user to
console.log(decision.model_selected);
console.log(decision.credits_estimated);

// Autocomplete while typing, and idle suggestions — both keyless:
console.log(await client.gabrielSuggest({ partial: "make me a log", tab: "image" }));
console.log(await client.gabrielRecommend({ credits_remaining: 40 }));

Translation

No authentication required.

const result = await client.translate({
  text: "The future of AI is collaborative and open",
  target_language: "pl",
});

console.log(result.translated_text);
console.log(result.source_language);   // "auto" unless you set source_language
console.log(result.character_count);

There is no formality, context or confidence — the endpoint takes text, target_language and optional source_language, nothing more. On a provider timeout it returns your input text unchanged instead of an error, so compare against the input if that distinction matters.


Billing & Usage

All amounts are USD.

const balance = await client.getBalance();
console.log(balance.credits.remaining_4h, "credits left in this 4h window");
console.log(balance.wallet.balance, balance.wallet.currency);   // USD

console.log(await client.getTransactions({ page: 1, pageSize: 20 }));
console.log(await client.getInvoices());
console.log(
  await client.estimateCost([{ type: "generate_image", model: "flux-2-pro", count: 10 }])
);

Per-endpoint analytics live at GET /v1/usage (JWT auth, fixed 30-day window) and have no SDK wrapper — see the Usage & Analytics docs.

Topping up. Take a fixed package or name your own amount:

for (const pkg of await client.getTopupPackages()) {
  // The slugs are historical — topup-50 is now the $15 package. Read
  // amount_usd, never the number in the slug.
  console.log(pkg.slug, pkg.amount_usd, `+${pkg.bonus_pct}% bonus credits`);
}

// topupWallet takes positional arguments, not an options object.
const session = await client.topupWallet(40);
console.log(session.checkout_url);

The wallet is denominated in USD, but a customer in Poland can pay in złoty — pass "pln" as the second argument and Stripe offers BLIK, card and bank transfer while the wallet is credited the USD amount you asked for:

const session = await client.topupWallet(40, "pln");

Storage

Dedicated S3 buckets live under /v1/storage/s3/* and are not wrapped by this SDK — call them with fetch for now:

const headers = {
  "Content-Type": "application/json",
  Authorization: `Bearer ${process.env.FOTOHUB_API_KEY}`,
};

const buckets = await (
  await fetch("https://apis.fotohub.app/v1/storage/s3/buckets", { headers })
).json();

const presigned = await (
  await fetch(
    `https://apis.fotohub.app/v1/storage/s3/buckets/${buckets[0].id}/objects/presign-upload`,
    {
      method: "POST",
      headers,
      body: JSON.stringify({
        key: "uploads/video.mp4",
        content_type: "video/mp4",
        expires_in: 3600,
      }),
    }
  )
).json();

// Upload directly to storage — no data passes through your server.
await fetch(presigned.url, { method: "PUT", body: fileBuffer });

Configuration

import { FotoHub } from "fotohub";

const client = new FotoHub({
  // Required
  apiKey: "fh_live_...",

  // Base URL (default: "https://apis.fotohub.app")
  baseUrl: "https://apis.fotohub.app",

  // Request timeout in ms (default: 60000)
  timeout: 30_000,

  // Max retry attempts for transient failures (default: 3)
  maxRetries: 5,

  // Custom fetch implementation (for testing, polyfills, or proxies)
  fetch: customFetchFn,
});

| Option | Type | Default | Description | |--------|------|---------|-------------| | apiKey | string | --- | Required. Your FOTOhub API key | | baseUrl | string | "https://apis.fotohub.app" | API base URL | | timeout | number | 60000 | Request timeout in milliseconds | | maxRetries | number | 3 | Max retry attempts for transient failures | | fetch | typeof fetch | globalThis.fetch | Custom fetch implementation |


Error Handling

The SDK provides 11 typed error classes for precise error handling:

import {
  FotoHub,
  FotoHubError,
  AuthenticationError,
  InsufficientCreditsError,
  RateLimitError,
  ValidationError,
  PermissionError,
  NotFoundError,
  TimeoutError,
  NetworkError,
  ServerError,
  JobFailedError,
  JobTimeoutError,
} from "fotohub";

try {
  const result = await client.generateImage({ prompt: "..." });
} catch (error) {
  if (error instanceof AuthenticationError) {
    // 401 --- Invalid or missing API key
    console.error("Authentication failed:", error.message);

  } else if (error instanceof InsufficientCreditsError) {
    // 402 --- Not enough credits for this operation
    console.error(
      `Need ${error.creditsRequired} credits, have ${error.creditsAvailable}`
    );

  } else if (error instanceof RateLimitError) {
    // 429 --- Too many requests
    console.error(`Rate limited. Retry after ${error.retryAfter}s`);

  } else if (error instanceof ValidationError) {
    // 422 --- Invalid request parameters
    console.error("Field errors:", error.fieldErrors);

  } else if (error instanceof PermissionError) {
    // 403 --- Insufficient permissions
    console.error("Forbidden:", error.message);

  } else if (error instanceof NotFoundError) {
    // 404 --- Resource not found
    console.error("Not found:", error.message);

  } else if (error instanceof TimeoutError) {
    // Request timed out (client-side)
    console.error("Timed out:", error.message);

  } else if (error instanceof NetworkError) {
    // Network connectivity issue
    console.error("Network error:", error.message, error.cause);

  } else if (error instanceof ServerError) {
    // 5xx --- Server-side error (auto-retried)
    console.error(`Server error (${error.statusCode}):`, error.message);

  } else if (error instanceof JobFailedError) {
    // Async generation job failed (e.g. waitFor3D)
    console.error(`Job ${error.jobId} failed:`, error.message);

  } else if (error instanceof JobTimeoutError) {
    // waitFor3D() exceeded maxWait
    console.error(`Job ${error.jobId} timed out`);

  } else if (error instanceof FotoHubError) {
    // Catch-all for any other API error
    console.error(`[${error.code}] ${error.message}`);
  }
}

Error Class Reference

| Class | HTTP Status | Properties | Description | |-------|-------------|------------|-------------| | FotoHubError | any | code, statusCode, details | Base error class | | AuthenticationError | 401 | --- | Invalid or missing API key | | InsufficientCreditsError | 402 | creditsRequired, creditsAvailable | Not enough credits | | PermissionError | 403 | --- | Insufficient permissions | | NotFoundError | 404 | --- | Resource not found | | ValidationError | 422 | fieldErrors | Invalid request parameters | | RateLimitError | 429 | retryAfter | Rate limit exceeded | | ServerError | 5xx | --- | Server-side error | | TimeoutError | --- | --- | Request timed out | | NetworkError | --- | cause | Network connectivity issue | | JobFailedError | --- | jobId | Async job failed | | JobTimeoutError | --- | jobId | Job polling exceeded maxWait |


Retry Behavior

The SDK automatically retries failed requests with exponential backoff:

| Condition | Retried? | |-----------|----------| | 429 Rate Limited | Yes | | 408 Timeout | Yes | | 5xx Server Error | Yes | | Network errors | Yes | | 409 with an idempotency key (your earlier attempt is still in flight) | Yes | | 401, 402, 403, 404, 409, 422 | No |

Backoff schedule: 1s, 2s, 4s (capped at 8s)

Idempotency — retries do not double-charge

Retrying a call that spends credits is only safe if the server can tell the retry apart from a new request. The dangerous case is a 504 or a timeout that arrives after the generation already started: the work is running and will be billed, but the client sees a failure.

Since 1.10.0 the SDK sends an X-Idempotency-Key on every request that can charge you, and every retry of one call reuses that same key. The API replays the original result instead of running the operation again, so a call that retried three times is still charged once. Nothing to configure.

// All three attempts inside this call share one idempotency key.
const result = await client.generateImage({ prompt: "Product photo" });

Two separate calls always get two different keys, even with identical arguments — asking twice means you want two generations, and collapsing them would lose one you paid for.

That scoping is deliberate, and it is also the limit of what the SDK can do for you: it protects the retries inside one call. If you need de-duplication across process restarts — a job queue that may redeliver the same work after a crash — the SDK cannot know two runs are the same job, so call the endpoint over HTTP with your own stable key (derive it from the job id):

await fetch("https://apis.fotohub.app/v1/ai/generate/image", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.FOTOHUB_API_KEY}`,
    "Content-Type": "application/json",
    "X-Idempotency-Key": `job-${jobId}`, // stable across restarts
  },
  body: JSON.stringify({ prompt: "Product photo" }),
});

Streaming endpoints (chat, gabriel, TTS, story) are excluded, because a stream cannot be buffered and replayed.

// Increase retries for critical workloads
const resilient = new FotoHub({
  apiKey: "fh_live_...",
  maxRetries: 5,
});

// Disable retries entirely
const noRetry = new FotoHub({
  apiKey: "fh_live_...",
  maxRetries: 0,
});

Runtime Compatibility

| Runtime | Minimum Version | Notes | |---------|----------------|-------| | Node.js | 18.0+ | Native fetch required | | Deno | 1.0+ | Works out of the box | | Bun | 1.0+ | Works out of the box | | Browsers | Modern (ES2020+) | Streaming requires ReadableStream | | Cloudflare Workers | --- | Full support | | Vercel Edge | --- | Full support |


TypeScript

The SDK is written in TypeScript and exports all types for full IntelliSense support:

import type {
  FotoHubConfig,
  GenerateImageOptions,
  GenerateIdaQOptions,
  ImageResult,
  ImageMetadata,
  EditImageOptions,
  EditResult,
  GenerateVideoOptions,
  VideoResult,
  GenerateMusicOptions,
  MusicResult,
  GenerateSpeechOptions,
  SpeechResult,
  TranscribeOptions,
  TranscriptionResult,
  ChatOptions,
  ChatMessage,
  ChatResult,
  ChatClaudeOptions,
  GabrielClassifyOptions,
  GabrielResult,
  TranslateOptions,
  TranslateResult,
  BillingBalance,
  WalletInfo,
  TopupPackage,
  CostOperation,
  CostEstimate,
  TransactionOptions,
  TransactionPage,
  StabilityTool,
  StabilityResult,
  Generate3DOptions,
  ThreeDResult,
  TryOnOptions,
  TryOnResult,
  Webhook,
} from "fotohub";

ChatStreamChunk and the ChatStream class are still exported, but only so existing imports keep compiling — chatStream() throws before it ever produces one. There is nothing to type against; use the raw /v1/ai/agent/stream frames from the Streaming section instead.


API Methods Reference

Every method takes a single options object unless the signature below shows positional arguments. Only four methods work without an API key.

Generation

| Method | Description | Key | |--------|-------------|:---:| | generateImage(options) | Generate images from a text prompt | Yes | | generateIdaQ(options) | Generate with IDA Q 1.0 (proprietary) | Yes | | editImage(options) | Edit an existing image from an instruction | Yes | | removeBackground(imageUrl) | Cut out the background | Yes | | upscaleImage(imageUrl, scale?) | Upscale (scale defaults to 2) | Yes | | analyzeImage(options) | Vision analysis of an image | Yes | | generateVideo(options) | Generate a video — synchronous, returns video_url. Not for Seedance | Yes | | generateSeedance(options) | Seedance 4–30s — submits and polls to completion | Yes | | registerVideoAsset(imageUrl) | Register a face for Seedance asset_ids — free | Yes | | waitForVideo(result, opts?) | Deprecated no-op; returns its input unchanged | Yes | | generateMusic(options) | Generate music from a description | Yes | | generateSfx(options) | Generate a sound effect | Yes | | generateSpeech(options) | Text-to-speech synthesis | Yes | | transcribe(options) | Transcribe audio to text | Yes | | generate3D(options) | Submit a 3D generation job | Yes | | get3DStatus(jobId) | Poll a 3D job | Yes | | waitFor3D(jobId, opts?) | Wait for a 3D job to finish | Yes | | list3DModels() | List available 3D models | Yes | | tryOn(options) | Submit a virtual try-on (one garment, or top + bottom) | Yes | | getTryOnStatus(jobId) | Poll a try-on job | Yes | | waitForTryOn(jobId, opts?) | Wait for a try-on job to finish | Yes |

Chat & orchestration

| Method | Description | Key | |--------|-------------|:---:| | chat(options) | Chat completion — one complete body, never a stream | Yes | | chatClaude(options) | Chat completion on a premium Claude model | Yes | | chatBedrock(options) | Chat completion via Bedrock routing | Yes | | chatStream(options) | Throws ValidationError — see Streaming | --- | | enhancePrompt(prompt, style?) | Rewrite a prompt for better output | Yes | | gabrielClassify(options) | Route a request to a model (returns a decision, not a completion) | Yes | | gabrielSuggest(options) | Prompt suggestions | No | | gabrielRecommend(options?) | Model recommendations | No | | translate(options) | Translate text (returns the input unchanged on failure) | No |

Stability AI tools

| Method | Description | Key | |--------|-------------|:---:| | listStabilityTools() | List available tools | Yes | | runStabilityTool(toolId, options) | Run any tool by id | Yes | | stabilityUpscale(...) | Upscale | Yes | | stabilityRemoveBackground(imageBase64) | Remove background | Yes | | stabilityErase(imageBase64, maskBase64) | Erase a masked region | Yes | | stabilityInpaint(...) | Inpaint a masked region | Yes | | stabilityOutpaint(imageBase64, padding) | Extend the canvas | Yes | | stabilitySearchReplace(...) | Replace an object by description | Yes | | stabilityRecolor(...) | Recolour an object | Yes | | stabilityStyleTransfer(...) | Apply a style reference | Yes |

Billing & tiers --- all amounts USD

| Method | Description | Key | |--------|-------------|:---:| | getBalance() | Credit balance, tier and overage config | Yes | | getCredits() | Credit totals only | Yes | | getWallet() | Wallet balance in USD | Yes | | topupWallet(amountUsd, payCurrency?) | Checkout for any amount; pass "pln" for BLIK | Yes | | getTopupPackages() | Fixed packages (read amount_usd, not the slug) | Yes | | createTopup(packageSlug) | Checkout for a fixed package | Yes | | setOverageLimit(hardLimitUsd, projectId?) | Cap overage spend | Yes | | estimateCost(operations) | Price a CostOperation[] before spending | Yes | | getTransactions(options?) | Paginated transaction history | Yes | | getInvoices() | Invoice list | Yes | | getPricing() | Public price catalogue | Yes | | listModels(category?) | Model catalogue | Yes | | getPlans() | API subscription plans | Yes | | getTierCatalog() | Public tier catalogue | No | | getCurrentTier() | Your tier, limits and usage | Yes | | compareTiers() | Your tier against the others | Yes | | subscribeTier(tierSlug) | Checkout for an API tier | Yes | | applyEnterprise(application) | Submit an enterprise application | Yes |

Webhooks

| Method | Description | Key | |--------|-------------|:---:| | listWebhooks() | List subscriptions | Yes | | createWebhook(options) | Create a subscription | Yes | | updateWebhook(webhookId, options) | Update a subscription | Yes | | deleteWebhook(webhookId) | Delete a subscription | Yes | | testWebhook(webhookId) | Send a test delivery | Yes | | getWebhookLogs(webhookId) | Recent delivery attempts | Yes |


Contributing

We welcome contributions. Please open an issue first to discuss what you would like to change.

# Clone the repository
git clone https://github.com/fotohubapp/sdk-node.git
cd sdk-node

# Install dependencies
npm install

# Build
npm run build

# Type check
npm run typecheck

Links


License

MIT --- Copyright (c) 2026 FOTOhub