@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
npm install @simpleq/sdkAuthenticate 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 point | What it exports | When you need it |
|---|---|---|
@simpleq/sdk | SimpleQ 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/express | simpleqWebhookHandler | Your worker is Express (express is an optional peer dependency). |
@simpleq/sdk/nest | SimpleQModule, SimpleQSignatureGuard, @SimpleQJob(), SimpleQBackpressureFilter | Your worker is Nest.js (@nestjs/common is an optional peer dependency). |
@simpleq/sdk/webhooks | verifyWebhook, verifyWebhookSignature, SIGNATURE_HEADER | Optional: 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
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
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:
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:
// 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.
Links
- GitHub — simpleqio/simpleq-sdk-typescript
- npm — @simpleq/sdk
- API reference — the full HTTP API, including queue and DLQ management; the SDK wraps the five job endpoints (
publish,getJob,ack,nack,defer) - Examples — complete runnable workers
- openapi.json — machine-readable API contract