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

@simpleq/sdk

v0.3.0

Published

Official Node/TypeScript SDK for SimpleQ — publish jobs, verify webhook signatures, and ack/nack/defer deliveries.

Readme

@simpleq/sdk

npm CI license

Official Node/TypeScript SDK for SimpleQ: publish jobs, verify webhook signatures, and run the ack-mode callbacks. ESM + CommonJS, bundled types, zero runtime dependencies. Requires Node 22+. All durations are in seconds. The live API contract is machine-readable at docs.simpleq.io/openapi.json.

Full SDK documentation: docs.simpleq.io/sdk.

npm install @simpleq/sdk

Client

An API key is required — pass apiKey, or set the SIMPLEQ_API_KEY environment variable and construct with no arguments. The constructor throws if neither is present.

import { SimpleQ } from '@simpleq/sdk';

// SIMPLEQ_API_KEY is read by the SDK automatically if no argument is passed
const simpleq = new SimpleQ({ apiKey: process.env.SIMPLEQ_API_KEY });

SimpleQ(options):

| Option | Required | Default | Meaning | | --- | --- | --- | --- | | apiKey | yes (here or via SIMPLEQ_API_KEY) | process.env.SIMPLEQ_API_KEY | API key (sq_live_…). | | timeout | no | 30 | Timeout for the SDK's own requests to the SimpleQ API, in seconds. | | maxRetries | no | 2 | How many times the SDK retries its own API request on a transient failure (network / 5xx / 429). |

timeout and maxRetries govern the SDK's calls to the SimpleQ API (publishing, ack/nack/defer, getJob). They do not control how SimpleQ retries delivery to your webhook — maxAttempts, backoff, rate limits, and ackTimeout are per-queue settings you configure when you create or edit a queue.

publish

Publish a job to a queue.

const job = await simpleq.publish('emails', {
  payload: { to: '[email protected]', template: 'welcome' },
  idempotencyKey: `welcome:${userId}`,
  delay: 30,
});
// job: {
//   id: string;
//   status: 'pending' | 'processing' | 'awaiting_ack' | 'completed' | 'dead';
//   createdAt: string;  // ISO 8601
// }

publish(queueName, params):

| Param | Required | Constraint | Meaning | | --- | --- | --- | --- | | payload | yes | JSON object | Delivered verbatim to the queue's webhook. | | idempotencyKey | no | ≤255 chars | Cross-call dedupe: publishing again with the same key returns the existing job. | | delay | no | seconds, 0–86400 | Defer first delivery. |

If you omit the idempotencyKey, the publish method generates one per call and reuses it across the SDK's automatic retries, so a retried request can never create a duplicate job. Pass your own key to dedupe across separate calls (e.g. one welcome email per user). A 201 (created — status pending) and a 200 (idempotent hit — returns the existing job, any status) both resolve as success.

Verifying webhooks

SimpleQ signs every delivery with an x-simpleq-signature header (HMAC-SHA256 of the raw body, keyed with the queue's signingSecret). Verify it before trusting the payload.

  • verifyWebhook(rawBody, header, signingSecret) — the framework-agnostic primitive. Returns the typed envelope or throws SignatureVerificationError. Use it anywhere: Lambda, Cloud Functions, Fastify, Hono, Next.js route handlers, raw http.
  • Framework adapters wrap that primitive for you, including the raw-body capture: Express via simpleqWebhookHandler (@simpleq/sdk/express), and Nest.js via SimpleQModule, SimpleQSignatureGuard, @SimpleQJob(), and SimpleQBackpressureFilter (@simpleq/sdk/nest).

signingSecret is per-queue. Each queue has its own — store each as its own env var (e.g. SQ_SIGNING_SECRET_EMAILS). A worker serving multiple queues should expose one webhook route per queue, each wired to that queue's secret (you can't safely pick the secret from the unverified body).

verifyWebhook — any framework

The primitive: verify the raw body and get back the typed envelope, or a SignatureVerificationError.

import { verifyWebhook } from '@simpleq/sdk';

const job = verifyWebhook(rawBody, signatureHeader, process.env.SQ_SIGNING_SECRET_EMAILS!);
// job: {
//   id: string;            // job ID — pass to ack/nack/defer
//   queue: string;         // queue name
//   payload: Record<string, unknown>;  // your data, verbatim
//   attempt: number;       // starts at 1
//   maxAttempts: number;
//   deferCount: number;    // backpressure holds so far — never counts against maxAttempts
//   createdAt: string;     // ISO 8601 — when the job was published
// }

rawBody is a string, Buffer, or Uint8Array. verifyWebhookSignature(rawBody, header, secret): boolean is the non-throwing variant.

Express — @simpleq/sdk/express

simpleqWebhookHandler captures the raw body, verifies the signature, and maps your handler's outcome to a response (see Response contract).

import express from 'express';
import { simpleqWebhookHandler } from '@simpleq/sdk/express';

const app = express();

app.post(
  '/webhook',
  simpleqWebhookHandler(process.env.SQ_SIGNING_SECRET_EMAILS!, async (job) => {
    // `job` is the verified, typed envelope. Resolve → 200.
    console.log(`processing job ${job.id} from queue ${job.queue}`);
  }),
);

Nest.js — @simpleq/sdk/nest

Create the app with rawBody: true, register SimpleQModule.forRoot, then guard the route and inject the typed job.

// main.ts
const app = await NestFactory.create(AppModule, { rawBody: true });
// app.module.ts
import { Module } from '@nestjs/common';
import { SimpleQModule } from '@simpleq/sdk/nest';

@Module({
  imports: [SimpleQModule.forRoot({ signingSecret: process.env.SQ_SIGNING_SECRET_EMAILS! })],
  controllers: [WebhookController],
})
export class AppModule {}
// webhook.controller.ts
import { Controller, HttpCode, Post, UseFilters, UseGuards } from '@nestjs/common';
import type { WebhookPayload } from '@simpleq/sdk';
import { SimpleQSignatureGuard, SimpleQJob, SimpleQBackpressureFilter } from '@simpleq/sdk/nest';

@Controller('webhook')
export class WebhookController {
  @Post()
  @HttpCode(200)
  @UseGuards(SimpleQSignatureGuard)
  @UseFilters(SimpleQBackpressureFilter)
  async handle(@SimpleQJob() job: WebhookPayload) {
    console.log(`processing job ${job.id} from queue ${job.queue}`);
  }
}

Response contract

The adapters (simpleqWebhookHandler, the Nest guard) map your handler's outcome to a SimpleQ response.

| Handler | Response | SimpleQ | | --- | --- | --- | | resolves | 200 | standard mode: completes the job · ack mode: moves it to awaiting_ack | | throw new SimpleQBackpressure(retryAfter?, { status? }) | that status (default 503) + Retry-After | holds and redelivers — no attempt burned | | throws anything else | 500 | counts a failed attempt, retries with backoff | | bad signature | 401 | not a job outcome |

Backpressure (a 429/503/529 from your downstream) tells SimpleQ to hold and redeliver the job without burning an attempt. In a standard-mode handler, throw a SimpleQBackpressure and the adapter turns it into the response; SimpleQBackpressure.from(err, { fallback }) builds one from the downstream error, relaying its status and Retry-After.

import { SimpleQBackpressure } from '@simpleq/sdk';

simpleqWebhookHandler(process.env.SQ_SIGNING_SECRET_EMAILS!, async (job) => {
  try {
    await doTheWork(job.payload); // your downstream call
  } catch (err) {
    if (err.status === 429 || err.status === 503 || err.status === 529) {
      throw SimpleQBackpressure.from(err, { fallback: 30 });
    }
    throw err; // anything else → 500, retried with backoff
  }
});

In ack mode you've already returned 200, so there's no response left to throw into — call simpleq.defer(...) out of band instead (see below).

Ack mode

For work longer than the 15s delivery window, use an ack-mode queue: return 200 immediately, then do the work and report the outcome with ack / nack / defer. Send the 200 yourself from the handler (via res), then run the work — the adapter won't respond again once you have.

import express from 'express';
import { SimpleQ, retryAfterSeconds } from '@simpleq/sdk';
import { simpleqWebhookHandler } from '@simpleq/sdk/express';

const simpleq = new SimpleQ(); // reads SIMPLEQ_API_KEY
const app = express();

app.post(
  '/webhook',
  simpleqWebhookHandler(process.env.SQ_SIGNING_SECRET_EMAILS!, async (job, { res }) => {
    res.status(200).end(); // ack mode: respond now, do the work after

    try {
      await doTheWork(job.payload); // your downstream call
      await simpleq.ack(job.id);
    } catch (err) {
      if (err.status === 429 || err.status === 503 || err.status === 529) {
        // Backpressure: hold and redeliver, no attempt burned. Relay the downstream's Retry-After.
        await simpleq.defer(job.id, { retryAfter: retryAfterSeconds(err) ?? 10 });
      } else if (err.status >= 400 && err.status < 500) {
        await simpleq.nack(job.id, { retryable: false, reason: `downstream ${err.status}` }); // dead-letter
      } else {
        await simpleq.nack(job.id, { retryable: true, reason: String(err) }); // retry with backoff
      }
    }
  }),
);

The three callbacks, each resolving to { id: string; accepted: true }:

| Callback | Effect | | --- | --- | | simpleq.ack(id) | Mark the job completed. | | simpleq.nack(id, { retryable?, reason? }) | Mark failed. retryable: false dead-letters immediately; default true retries with backoff. reason ≤500 chars. | | simpleq.defer(id, { retryAfter, reason? }) | Backpressure: held and redelivered, no attempt burned. retryAfter seconds (≥ 0, no fixed ceiling — pass a provider's retry hint straight through); reason ≤500 chars. Two platform bounds apply: the queue's maxDefers budget (default: 50 holds per job → defer_cap_exhausted) and the job's 24-hour delivery lifetime (an over-lifetime hold dead-letters immediately with retry_after_exceeds_lifetime). The call succeeds either way — the job resolves to held or dead-lettered. |

getJob

Fetch a job's current status and attempt history.

const job = await simpleq.getJob(jobId);
// job: {
//   id: string;
//   queue: string;                       // queue name
//   status: 'pending' | 'processing' | 'awaiting_ack' | 'completed' | 'dead';
//   attempts: number;
//   maxAttempts: number;
//   idempotencyKey: string | null;
//   payload: Record<string, unknown>;
//   scheduledFor: string;                // ISO 8601
//   lastError: string | null;
//   history: Array<{
//     attempt: number;
//     status: 'success' | 'failed' | 'nacked' | 'deferred';
//     error: string | null;
//     webhookStatusCode: number | null;
//     timestamp: string;                 // ISO 8601
//   }>;
//   createdAt: string;                   // ISO 8601
//   completedAt: string | null;          // ISO 8601
// }

Retention

Once a job reaches a terminal state (completed or dead), its record — status and the full per-attempt history — is purged at its 72-hour-from-publish deadline (on the next sweep pass), after which getJob throws NotFoundError (.status === 404). Dead-letter entries are removed at the same deadline, so a job that dies late in its life has correspondingly little replay time left. Idempotency keys dedupe for the job's retention lifetime: once the original job ages out, the same key creates a fresh job.

Errors

Every thrown value extends SimpleQError. API responses throw ApiError (or a subclass) carrying .status and .body.

| Class | Thrown when | Extra fields | | --- | --- | --- | | AuthenticationError | 401/403 — missing, invalid, or revoked API key | .status, .body | | ValidationError | 400 — request validation failed | .status, .body (field-level details) | | NotFoundError | 404 — queue or job not found | .status, .body | | RateLimitError | 429 — rate limited | .status, .body, .retryAfter (seconds) | | ApiError | any other non-2xx | .status, .body | | SimpleQConnectionError | network failure or timeout | — | | SignatureVerificationError | webhook signature mismatch | — | | SimpleQError | base class — catch-all (also config errors, e.g. missing API key) | — |

import { ApiError, NotFoundError, RateLimitError, SimpleQError } from '@simpleq/sdk';

try {
  await simpleq.publish('emails', { payload: { to: '[email protected]' } });
} catch (err) {
  if (err instanceof NotFoundError) {
    console.error('Queue does not exist:', err.body);
  } else if (err instanceof RateLimitError) {
    console.error(`Rate limited; retry after ${err.retryAfter}s`);
  } else if (err instanceof ApiError) {
    console.error(`SimpleQ API error ${err.status}:`, err.body);
  } else if (err instanceof SimpleQError) {
    console.error('Network, signature, or config error:', err.message);
  } else {
    throw err;
  }
}

Links