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

hiapi_ai

v0.2.1

Published

Zero-dependency Node.js client for the HiAPI unified async task API (/v1/tasks).

Readme

HiAPI Node.js SDK

Zero-dependency Node.js client for the HiAPI unified async task API (/v1/tasks) — submit an image / video / audio generation task, poll it to completion, and read the output in one call.

  • Zero runtime dependencies. Built on the global fetch and node:crypto — nothing else.
  • One-call workflow. client.tasks.run(...) submits and waits for you.
  • TypeScript-first. Full type declarations, ESM and CommonJS entry points.
  • Webhook verification. HMAC-SHA256 signature + timestamp freshness check, built in (still deduplicate deliveries by task id).

For OpenAI-compatible chat/image endpoints, keep using the openai library with baseURL: "https://api.hiapi.ai/v1". This SDK focuses on what the OpenAI client can't do: the asynchronous submit → poll → retrieve lifecycle (results come back as output URLs).

Install

npm install hiapi

Requires Node.js 18.17+. On Node 18–20, the first request prints a one-time ExperimentalWarning: The Fetch API is an experimental feature to stderr — this comes from Node's own global fetch (stable since Node 21), not from this package, and is harmless.

Quick start

import { HiAPI } from "hiapi";
// CommonJS works too: const { HiAPI } = require("hiapi");

const client = new HiAPI({ apiKey: "sk-..." }); // or set HIAPI_API_KEY

const task = await client.tasks.run({
  model: "seedance-2-0",
  input: { prompt: "a cyan glass data center entrance", resolution: "1080p" },
  onUpdate: (t) => console.log("status:", t.status),
});

for (const out of task.output) {
  console.log(out.type, out.url); // e.g. "video https://cdn.hiapi.ai/tasks/..."
}

run() resolves once the task reaches a terminal state. It throws TaskFailedError if the task fails and PollTimeoutError once the timeoutMs polling budget (default 600000 — 10 minutes) is used up — checked between polls, not a hard cutoff, so a slow individual request can push the actual wait somewhat past timeoutMs. A poll timeout does not cancel the task — it may still finish (and bill) later; take taskId from the PollTimeoutError and retrieve() it instead of submitting the same request again.

timeoutMs does not bound onUpdate itself for the final (success/ fail) call — by the time the task reaches a terminal state, the SDK already has the real result in hand, and there's no safe way to give up on an in-flight onUpdate call without either hanging anyway or silently discarding whatever it was doing (e.g. persisting the result). It's awaited to completion instead. If your onUpdate does work that could itself hang, bound it with signal:

const task = await client.tasks.run({
  model: "seedance-2-0",
  input: { prompt: "..." },
  signal: AbortSignal.timeout(120_000), // also bounds a hanging onUpdate
  onUpdate: async (t) => {
    await saveTaskState(t); // if this hangs, `signal` is what stops the wait
  },
});

Aborting stops wait()/run() from waiting any further — it does not cancel the remote task, or any onUpdate call already in progress.

Output URLs are temporary — they expire about 7 days after creation (each output carries expireAt). To keep an output long-term, pass storage: "persistent" to create()/run() (billed by size; insufficient balance or exceeding your storage cap silently downgrades to "temp" — the actual tier used is echoed back as task.storage), or promote it after the fact — see the Output Storage docs.

Lower-level control

const created = await client.tasks.create({
  model: "seedance-2-0",
  input: { prompt: "...", resolution: "720p" },
  callback: { url: "https://your-app.com/hiapi/callback", when: "final" },
});
console.log(created.taskId);

let task = await client.tasks.retrieve(created.taskId); // one status check
task = await client.tasks.wait(created.taskId, { pollIntervalMs: 3000, timeoutMs: 900000 });
const page = await client.tasks.list({ page: 1, size: 20 }); // newest first

input fields are defined per model — see the relevant model page. Don't put callback fields inside input; pass callback separately.

Model routes

Some models expose multiple routes (e.g. ext) with different pricing or upstream capacity. Pass route instead of writing the model@route suffix:

const created = await client.tasks.create({
  model: "gpt-image-2/text-to-image",
  route: "ext", // preferred over model: "...@ext"
  input: { prompt: "..." },
});

Omitting route (or passing "default") uses the model's default route. An unknown route fails fast with a 400 whose message lists the available routes. The legacy model: "x@ext" spelling keeps working. When a task was submitted with route, its detail echoes task.route and task.model holds the resolved full name (x@ext).

Idempotent retries

Pass idempotencyKey (sent as the Idempotency-Key header — printable ASCII, ≤255 characters) to make task submission safe to retry — retrying the same key + same body within about 24 hours returns the first task instead of creating and billing a new one (after that the key is cleaned up and the same request creates a new task):

const created = await client.tasks.create({
  model: "seedance-2-0",
  input: { prompt: "..." },
  idempotencyKey: "order-8472:video", // a stable key you derive per job
});
if (created.idempotentReplay) {
  console.log("hit the idempotency cache; no new task created");
}

With a key set, the SDK also retries the POST on network errors and retries 409 IDEMPOTENCY_KEY_PROCESSING (the first request is still in flight) up to the retry limit (default 2), honouring Retry-After capped at 60s. Reusing a key with a different body throws IdempotencyKeyMismatchError — that's a key-construction bug, not retryable.

Webhooks

If you set a Webhook signing key in the HiAPI console, terminal callbacks are signed. Verify them against the raw request body:

import { createServer } from "node:http";
import { HiAPI, WebhookVerificationError } from "hiapi";

const client = new HiAPI({ apiKey: "sk-...", webhookSecret: "whsec_..." });
const MAX_BODY = 1024 * 1024; // 1 MiB — reject oversized bodies before verifying

createServer((req, res) => {
  if (req.method !== "POST" || req.url !== "/hiapi/callback") {
    res.writeHead(404).end();
    return;
  }
  const chunks = [];
  let size = 0;
  req.on("data", (chunk) => {
    size += chunk.length;
    if (size > MAX_BODY) {
      res.writeHead(413).end();
      req.destroy();
      return;
    }
    chunks.push(chunk);
  });
  req.on("end", () => {
    let task;
    try {
      task = client.webhooks.verify(Buffer.concat(chunks), req.headers);
    } catch (err) {
      if (err instanceof WebhookVerificationError) {
        res.writeHead(400).end();
        return;
      }
      throw err;
    }
    if (task.succeeded && task.output.length > 0) {
      console.log(task.output[0].url);
    }
    res.writeHead(200).end(); // ack with 2xx; HiAPI retries non-2xx
  });
}).listen(3000);

If you use a framework, make sure the handler receives the exact bytes of the request body (e.g. express.raw({ type: "application/json" }) in Express) — verifying a re-serialized JSON object can fail even when the payload is legitimate.

Callbacks are delivered at least once and can arrive concurrently — make your handler idempotent (e.g. upsert your own record keyed by task.taskId) and return 2xx only after processing succeeds. Duplicates are then harmless, and a failed or crashed handler is simply redelivered.

Errors

| Error class | When | | --- | --- | | AuthenticationError | 401 — bad/missing API key | | NotFoundError | 404 — unknown task or not yours | | InvalidRequestError | INVALID_REQUEST — fix the request | | ModelUnavailableError | MODEL_UNAVAILABLE — retry or switch model | | TaskFailedSyncError | TASK_FAILED — the submission was rejected synchronously (distinct from TaskFailedError below) | | TaskTimeoutError / StorageUnavailableError | retryable upstream errors | | ServiceUnavailableError | 503 — platform busy (auto-retried) | | IdempotencyKeyProcessingError | 409 — same key still in flight (auto-retried; retryable) | | IdempotencyKeyMismatchError | 422 — key reused with a different body (not retryable) | | APIError | any other non-2xx response (base class — e.g. 402, 403; carries status and body) | | APIConnectionError | network failure or client-side timeout (auto-retried for reads only — not a keyless create()) | | TaskFailedError | a polled task ended in status=fail (carries task and code) | | PollTimeoutError | run()/wait() exceeded its timeout (carries taskId) | | RunAbortedError | run()'s signal was aborted right after its task was created (carries taskId — the task exists and may still bill) | | RunFailedError | run()'s task exists but failed some other way while polling — network/API error, an onUpdate rejection, a later abort (carries taskId and the original error as cause) | | WebhookVerificationError | bad signature or stale timestamp |

429/503 are retried automatically with exponential backoff (maxRetries, default 2; honours Retry-After). Network errors are retried only for idempotent GETs (retrieve / list, and the polling inside wait / run). The POST that create() issues is never retried on a network failure — unless you pass idempotencyKey, which makes the retry safe server-side. Without a key, an APIConnectionError from create() leaves the request in an unknown state — a task may or may not have been created (and billed). list() can help you inspect recent tasks manually, but tasks don't echo your input back, so absence from the list doesn't prove the request failed — don't retry automatically on that basis. For anything automated, submit with an idempotencyKey so the retry is safe by design.

Configuration

new HiAPI({
  apiKey: undefined,                  // falls back to HIAPI_API_KEY
  baseUrl: "https://api.hiapi.ai/v1",
  timeoutMs: 60_000,                  // per-request
  maxRetries: 2,
  webhookSecret: undefined,           // falls back to HIAPI_WEBHOOK_SECRET
  fetch: undefined,                   // custom fetch impl (testing/instrumentation)
});

This SDK is designed for server-side use. Don't ship your API key to browsers — route generation through your backend instead.

License

MIT