Skip to content

@simpleq/sdk — the official Node/TypeScript SDK for SimpleQ

@simpleq/sdk is the official Node/TypeScript SDK for SimpleQ, the managed job queue for AI and API-heavy backends. Install with npm install @simpleq/sdk. It publishes jobs with automatic idempotent retries, verifies x-simpleq-signature webhook headers, and reports ack-mode outcomes with ack / nack / defer. Node 22+, ESM + CommonJS, bundled types, zero runtime dependencies, MIT-licensed, with Express and Nest.js adapters.

Install

bash
npm install @simpleq/sdk

Authenticate with an API key from the dashboard under API Keys — set the SIMPLEQ_API_KEY environment variable and call new SimpleQ(), or pass it explicitly as new SimpleQ({ apiKey }). SIMPLEQ_API_KEY is the only variable the SDK reads on its own; the samples below pass the key explicitly from a shell variable named SQ_API_KEY (an app-chosen name, like SQ_SIGNING_SECRET). All durations are in seconds.

Start with @simpleq/sdk — publishing, verification, and ack/nack/defer all come from the root. A worker on Express or Nest.js adds the one matching adapter; a worker on any other framework (Fastify, Hono, Next.js, AWS Lambda, or raw node:http) uses verifyWebhook from the root, no adapter needed.

Entry pointWhat it exportsWhen you need it
@simpleq/sdkSimpleQ client (publish, getJob, ack, nack, defer), verifyWebhook, verifyWebhookSignature, SIGNATURE_HEADER, retryAfterSeconds, SimpleQBackpressure, error classes, and all public types (WebhookPayload, Job, …)Any app — publishing, verification, and ack/nack/defer all live here.
@simpleq/sdk/expresssimpleqWebhookHandlerYour worker is Express (express is an optional peer dependency).
@simpleq/sdk/nestSimpleQModule, SimpleQSignatureGuard, @SimpleQJob(), SimpleQBackpressureFilterYour worker is Nest.js (@nestjs/common is an optional peer dependency).
@simpleq/sdk/webhooksverifyWebhook, verifyWebhookSignature, SIGNATURE_HEADEROptional: a verify-only worker that never publishes — pulls in only node:crypto. The same functions are also exported from the root.

Quickstart

Publish a job

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

// SQ_API_KEY is an app-chosen env var; only SIMPLEQ_API_KEY is auto-read by new SimpleQ().
const simpleq = new SimpleQ({ apiKey: process.env.SQ_API_KEY });

const job = await simpleq.publish('my-queue', {
  payload: { task: 'hello' },
  idempotencyKey: 'task-001', // optional — auto-generated when omitted
  delay: 30, // optional — seconds before first delivery (max 86400)
});
// → { id, status, createdAt }

publish() attaches an idempotency key to every request — yours or a generated one — and reuses it across the SDK's built-in retries (maxRetries, default 2), so a retried publish can never create a duplicate job. See Idempotency.

Verify a webhook

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

// Verifies the signature, THEN parses — throws SignatureVerificationError on mismatch.
const job = verifyWebhook(rawBody, req.headers['x-simpleq-signature'], process.env.SQ_SIGNING_SECRET);
// → { id, queue, payload, attempt, maxAttempts, createdAt }

HMAC-SHA256 over the raw request body with the queue's signingSecret, constant-time compare included. See Signature verification.

Ack, nack, or defer (ack mode)

Ack mode is for work that can outlive SimpleQ's 15-second webhook window. Respond 200 as soon as the delivery lands — the response itself must beat that window — then do the work out of band and report the real outcome:

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

const simpleq = new SimpleQ({ apiKey: process.env.SQ_API_KEY });
const app = express();

// Replace with your real work. Throw an error with a numeric `.status` to route failures.
async function doTheWork(payload) {
  // your job logic here
}

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

    try {
      await doTheWork(job.payload);
      await simpleq.ack(job.id);
    } catch (err) {
      const status = err?.status;
      if (status === 429 || status === 503 || status === 529) {
        // Backpressure: the job is held and redelivered — no attempt burned.
        await simpleq.defer(job.id, { retryAfter: retryAfterSeconds(err) ?? 10, reason: `downstream ${status}` });
      } else if (status >= 400 && status < 500) {
        await simpleq.nack(job.id, { retryable: false, reason: `downstream ${status}` }); // dead-letter
      } else {
        await simpleq.nack(job.id, {
          retryable: true, // retry with backoff
          reason: `downstream ${status ?? 'network'}: ${err?.message ?? 'unknown'}`,
        });
      }
    }
  }),
);

app.listen(9000);

ack, nack, and defer are methods on the SimpleQ client that take the job id — call them from anywhere, not just inside the webhook handler. defer requires retryAfter (seconds, ≥ 0 — bounded by the job's 24-hour delivery lifetime, not a fixed ceiling). If the process crashes between the 200 and the report, the queue's ackTimeout with ackTimeoutAction: "retry" (the default) redelivers the job, so nothing is silently dropped. See Ack mode and Backpressure; the same worker with Python, Go, Java, PHP, and C#/.NET equivalents is Generic ack worker.

Framework adapters

Express

simpleqWebhookHandler(signingSecret, handler) (from @simpleq/sdk/express) captures the raw body, verifies the signature (401 on mismatch), and runs your handler with (job, { req, res }). In ack mode you send the 200 yourself and report the outcome out of band, as in the quickstart above. In standard (synchronous) mode you let the handler resolve — it responds 200 — or throw: throw SimpleQBackpressure.from(err, { fallback: 10 }) responds with the error's status (429/503/529, default 503) plus a Retry-After header, and any other throw responds 500. Standard-mode worker: Standard mode queue.

Nest.js

@simpleq/sdk/nest provides SimpleQModule.forRoot({ signingSecret }) (or forRootAsync), the SimpleQSignatureGuard route guard, the @SimpleQJob() parameter decorator, and SimpleQBackpressureFilter. It runs inside a standard Nest install — npm install @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata rxjs alongside @simpleq/sdk (the SDK declares only @nestjs/common as an optional peer; the rest come with every Nest app). A complete ack-mode worker in one file:

ts
// main.ts
import { Controller, HttpCode, Module, Post, UseGuards } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { SimpleQ, type WebhookPayload, retryAfterSeconds } from '@simpleq/sdk';
import { SimpleQModule, SimpleQSignatureGuard, SimpleQJob } from '@simpleq/sdk/nest';

const simpleq = new SimpleQ({ apiKey: process.env.SQ_API_KEY });

// Replace with your real work. Throw an error with a numeric `.status` to route failures.
async function doTheWork(payload: Record<string, unknown>) {
  // your job logic here
}

@Controller('webhook')
class WebhookController {
  // Ack mode: the guard verifies the signature, we return 200 immediately (@HttpCode),
  // then report the real outcome out of band.
  @Post()
  @HttpCode(200)
  @UseGuards(SimpleQSignatureGuard)
  handle(@SimpleQJob() job: WebhookPayload) {
    void processJob(job);
  }
}

async function processJob(job: WebhookPayload) {
  try {
    await doTheWork(job.payload);
    await simpleq.ack(job.id);
  } catch (err: any) {
    const status = err?.status;
    if (status === 429 || status === 503 || status === 529) {
      // Backpressure: the job is held and redelivered — no attempt burned.
      await simpleq.defer(job.id, { retryAfter: retryAfterSeconds(err) ?? 10, reason: `downstream ${status}` });
    } else if (status >= 400 && status < 500) {
      await simpleq.nack(job.id, { retryable: false, reason: `downstream ${status}` }); // dead-letter
    } else {
      await simpleq.nack(job.id, {
        retryable: true, // retry with backoff
        reason: `downstream ${status ?? 'network'}: ${err?.message ?? 'unknown'}`,
      });
    }
  }
}

@Module({
  imports: [SimpleQModule.forRoot({ signingSecret: process.env.SQ_SIGNING_SECRET! })],
  controllers: [WebhookController],
})
class AppModule {}

// rawBody: true lets the guard verify the signature against the exact bytes received.
const app = await NestFactory.create(AppModule, { rawBody: true });
await app.listen(9000);

processJob is the same ack/nack/defer decision tree as the Express quickstart — only the immediate 200 differs: Express sends it with res.status(200).end() inside the handler, while Nest sends it when handle() returns (@HttpCode(200)), which is why the work is kicked off with void rather than awaited. Provider-specific versions of this worker: OpenAI queue and Anthropic queue — each includes a Nest.js tab alongside Express and Python (FastAPI).

Is there a Python, Go, Java, PHP, or C#/.NET SDK?

No — TypeScript is the only official SDK today. The SimpleQ API is HTTP-first: any language that can send an HTTP request and compute an HMAC-SHA256 can publish jobs and run a worker. Complete reference workers in Python (FastAPI), Go, Java (Spring Boot), PHP, and C#/.NET are in Standard mode queue and Generic ack worker, and the machine-readable openapi.json can generate a client in any language. More official SDKs are coming.