qstash-js
Work with the QStash JavaScript/TypeScript SDK for serverless messaging, scheduling. Use when publishing messages to HTTP endpoints, creating schedules, managing queues, verifying incoming messages in serverless environments.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install upstash-qstash-js-skills
Repository
Skill path: skills
Work with the QStash JavaScript/TypeScript SDK for serverless messaging, scheduling. Use when publishing messages to HTTP endpoints, creating schedules, managing queues, verifying incoming messages in serverless environments.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: upstash.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install qstash-js into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/upstash/qstash-js before adding qstash-js to shared team environments
- Use qstash-js for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: qstash-js
description: Work with the QStash JavaScript/TypeScript SDK for serverless messaging, scheduling. Use when publishing messages to HTTP endpoints, creating schedules, managing queues, verifying incoming messages in serverless environments.
---
# QStash JavaScript SDK
QStash is an HTTP-based messaging and scheduling solution for serverless and edge runtimes. This skill helps you use the QStash JS SDK effectively.
## When to use this skill
Use this skill when:
- Publishing HTTP messages to endpoints or URL groups
- Creating scheduled or delayed message delivery
- Managing FIFO queues with configurable parallelism
- Verifying incoming webhook signatures from QStash
- Implementing callbacks, DLQ handling, or message deduplication
## Quick Start
### Installing the SDK
```bash
npm install @upstash/qstash
```
### Basic Publishing
```typescript
import { Client } from "@upstash/qstash";
const client = new Client({
token: process.env.QSTASH_TOKEN!,
});
const result = await client.publishJSON({
url: "https://my-api.example.com/webhook",
body: { event: "user.created", userId: "123" },
});
```
## Core Concepts
For fundamental QStash operations, see:
- [Publishing Messages](fundamentals/publishing-messages.md)
- [Schedules](fundamentals/schedules.md)
- [Queues and Flow Control](fundamentals/queues-and-flow-control.md)
- [URL Groups](fundamentals/url-groups.md)
For verifying incoming messages:
- [Receiver Verification](verification/receiver.md) - Core signature verification with the Receiver class
- Platform-Specific Verifiers:
- [Next.js](verification/platform-specific/nextjs.md) - App Router, Pages Router, and Edge Runtime
For advanced features:
- [Callbacks](advanced/callbacks.md)
- [Dead Letter Queue (DLQ)](advanced/dlq.md)
- [Message Deduplication](advanced/deduplication.md)
- [Region migration & multi-region support](advanced/multi-region.md)
- If needed, [multi-region env variable setup verification script](advanced/multi-region/verify-multi-region-setup.ts). Can be run without arguments
## Platform Support
QStash JS SDK works across various platforms:
- Next.js (App Router and Pages Router)
- Cloudflare Workers
- Deno
- Node.js (v18+)
- Vercel Edge Runtime
- SvelteKit, Nuxt, SolidJS, and other frameworks
> **Note on Workflow SDK:** For building complex durable workflows that chain multiple QStash messages together, consider using the separate QStash Workflow SDK (`@upstash/workflow`). The Workflow SDK empowers you to orchestrate multi-step processes with automatic state management, retries, and fault tolerance. This Skills file focuses on the core QStash messaging SDK.
## Best Practices
- Always verify incoming QStash messages using the Receiver class
- Use environment variables for tokens and signing keys
- Set appropriate retry counts and timeouts for your use case
- Use queues for ordered processing with controlled parallelism
- Implement DLQ handling for failed message recovery
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### fundamentals/publishing-messages.md
```markdown
# Publishing Messages
Publish HTTP messages to destinations using the QStash SDK. Messages are delivered asynchronously with built-in retries and monitoring.
## Basic Publishing
### publishJSON()
```typescript
import { Client } from "@upstash/qstash";
const client = new Client({ token: process.env.QSTASH_TOKEN! });
const result = await client.publishJSON({
url: "https://api.example.com/webhook",
body: { userId: "123", event: "order.completed" },
});
console.log(result.messageId); // "msg_123..."
```
## Destination Options
Specify where to send the message using one of these mutually exclusive options:
```typescript
await client.publishJSON({
// Send to a single HTTP endpoint:
url: "https://api.example.com/webhook",
// Send to all endpoints in a URL group. Creates one message per endpoint:
// Learn more in [URL Groups](url-groups.md).
urlGroup: "my-api-group",
// ...
});
```
## Message Options
```typescript
// JSON object
await client.publishJSON({
url: "https://api.example.com/webhook",
// The message payload
body: { order_id: "123", items: [1, 2, 3] },
// Send to a FIFO queue for ordered processing:
// Learn more in [Queues and Flow Control](queues-and-flow-control.md).
queueName: "my-fifo-queue",
// Send with a flow control key to limit rate/parallelism:
flowControl: {
key: "user-123",
parallelism: 2,
rate: 10,
period: 60,
},
// request headers
headers: {
"Content-Type": "application/json",
"X-Custom-Header": "value",
Authorization: "Bearer token", // auth token for the destination
},
// request method
method: "PUT",
// Delay message delivery by a duration in seconds:
delay: 60,
// alternative of delay, deliver at a specific Unix timestamp in seconds:
notBefore: 1700000000,
// retries
retries: 10,
// Customize the delay between retries using a mathematical expression.
// Default is exponential backoff.
// Supported functions: `pow`, `sqrt`, `abs`, `exp`, `floor`, `ceil`, `round`, `min`, `max`.
retryDelay: "5000",
// Maximum duration for the HTTP request in seconds:
timeout: 15,
// URL called if the message is successfully delivered:
callback: "https://api.example.com/qstash-callback",
// URL called only when all retries are exhausted:
failureCallback: "https://api.example.com/failure-handler",
// id for deduplicating messages
deduplicationId: "custom-id-123",
// enable content-based deduplication
contentBasedDeduplication: true,
// label for filtering logs, dlq, cancellation
label: "order-webhook",
});
```
## Batch Publishing
Publish multiple messages in a single request:
```typescript
const results = await client.batchJSON([
{
url: "https://api.example.com/webhook-1",
body: { event: "first" },
},
{
url: "https://api.example.com/webhook-2",
body: { event: "second" },
delay: 60,
},
{
urlGroup: "my-group",
body: { event: "third" },
},
]);
```
Each message in the batch can have different options.
## Response Types
### Single URL
When publishing to a `url`, response contains:
```typescript
{
messageId: string; // Unique message identifier
url: string; // Destination URL
deduplicated?: boolean; // true if message was deduplicated
}
```
### URL Group
When publishing to a `urlGroup`, response contains an array:
```typescript
[
{
messageId: string;
url: string; // First endpoint URL
deduplicated?: boolean;
},
{
messageId: string;
url: string; // Second endpoint URL
deduplicated?: boolean;
},
// ... one per endpoint
]
```
```
### fundamentals/schedules.md
```markdown
# Schedules
Schedule recurring messages using cron expressions.
## Creating Schedules
```typescript
import { Client } from "@upstash/qstash";
const client = new Client({ token: process.env.QSTASH_TOKEN! });
const result = await client.schedules().create({
destination: "https://api.example.com/daily-report",
cron: "0 9 * * *", // Daily at 9 AM UTC
body: JSON.stringify({ report: "daily" }),
headers: { "Content-Type": "application/json" },
});
console.log(result.scheduleId);
```
- `destination`: URL or URL group name
- `cron`: Cron expression (required)
- `body`: Message payload
- All publish options supported (retries, timeout, callback, etc.)
## Common Cron Patterns
```
0 * * * * Every hour
0 9 * * * Daily at 9 AM UTC
0 9 * * 1 Weekly on Monday at 9 AM
0 9 1 * * Monthly on 1st at 9 AM
*/15 * * * * Every 15 minutes
0 9-17 * * 1-5 Weekdays, 9 AM to 5 PM, hourly
0 0 1 1 * Annually on January 1st
```
Format: `minute hour day month weekday`
- Minute: 0-59
- Hour: 0-23 (UTC)
- Day: 1-31
- Month: 1-12
- Weekday: 0-6 (Sunday=0)
## Managing Schedules
### List All Schedules
```typescript
const schedules = await client.schedules().list();
schedules.forEach((s) => {
console.log(`${s.scheduleId}: ${s.cron} -> ${s.destination}`);
console.log(` Next run: ${new Date(s.nextScheduleTime!)}`);
console.log(` Paused: ${s.isPaused}`);
});
```
### Get Schedule Details
```typescript
const schedule = await client.schedules().get("scd_123...");
console.log(schedule.cron);
console.log(schedule.destination);
console.log(schedule.retries);
console.log(schedule.body); // Base64 encoded
```
### Delete Schedule
```typescript
await client.schedules().delete("scd_123...");
```
### Pause and Resume
```typescript
// Pause - stops scheduling new messages
await client.schedules().pause({ scheduleId: "scd_123..." });
// Resume - restarts scheduling
await client.schedules().resume({ scheduleId: "scd_123..." });
```
In-flight messages continue when paused.
## Schedule with Options
```typescript
await client.schedules().create({
destination: "https://api.example.com/cleanup",
cron: "0 2 * * *", // Daily at 2 AM
body: JSON.stringify({ task: "cleanup" }),
headers: { "Content-Type": "application/json" },
retries: 3,
timeout: 120,
callback: "https://api.example.com/schedule-callback",
failureCallback: "https://api.example.com/schedule-failure",
label: "nightly-cleanup",
});
```
## Schedule to URL Group
```typescript
await client.schedules().create({
destination: "status-checkers", // URL group name
cron: "*/5 * * * *", // Every 5 minutes
body: JSON.stringify({ check: "health" }),
});
```
Schedule creates one message per endpoint in the group on each trigger.
## Schedule to Queue
```typescript
await client.schedules().create({
destination: "https://api.example.com/process",
cron: "0 * * * *",
queueName: "hourly-tasks",
body: JSON.stringify({ task: "process" }),
});
```
Messages are enqueued for ordered FIFO delivery. See [Queues](queues-and-flow-control.md).
## Updating Schedules
To update, provide the existing `scheduleId`:
```typescript
await client.schedules().create({
scheduleId: "scd_123...", // Existing schedule ID
destination: "https://api.example.com/updated-endpoint",
cron: "0 10 * * *", // New time
body: JSON.stringify({ updated: true }),
});
```
All fields are replaced with new values.
## Deduplication
Prevent duplicate schedules:
```typescript
await client.schedules().create({
destination: "https://api.example.com/daily",
cron: "0 9 * * *",
deduplicationId: "daily-report-schedule",
body: JSON.stringify({ report: "daily" }),
});
```
Deduplication happens before schedule creation. See [Deduplication](../advanced/deduplication.md).
## Schedule Tracking
Schedule metadata includes execution history:
```typescript
const schedule = await client.schedules().get("scd_123...");
console.log(schedule.lastScheduleTime); // Last execution time
console.log(schedule.nextScheduleTime); // Next execution time
console.log(schedule.lastScheduleStates); // Recent message states
```
`lastScheduleStates` maps message IDs to states:
- `IN_PROGRESS`: Currently delivering
- `SUCCESS`: Successfully delivered
- `FAIL`: Failed after retries
```
### fundamentals/queues-and-flow-control.md
```markdown
# Queues and Flow Control
## Queues
Queues provide FIFO (First-In-First-Out) ordered message delivery with configurable parallelism.
### Basic Queue Usage
```typescript
import { Client } from "@upstash/qstash";
const client = new Client({ token: process.env.QSTASH_TOKEN! });
// Enqueue a message
await client.queue({ queueName: "orders" }).enqueueJSON({
url: "https://api.example.com/process-order",
body: { orderId: "123", items: ["item1", "item2"] },
});
```
Messages are delivered in order, one at a time by default.
### Ordering Guarantees
- Messages delivered in FIFO order
- Next message waits for current message to be delivered or fail
- Next message waits for callbacks to complete
- Retries don't break ordering (next waits for all retries)
### Configuring Parallelism
Control how many messages process concurrently:
```typescript
// Create or update queue with parallelism
await client.queue({ queueName: "orders" }).upsert({
parallelism: 5, // Process up to 5 messages concurrently
});
// Then enqueue messages
await client.queue({ queueName: "orders" }).enqueueJSON({
url: "https://api.example.com/process",
body: { task: "data" },
});
```
**Note:** Queue parallelism is being deprecated. Use Flow Control for rate limiting (see below).
### Managing Queues
```typescript
// Get queue details
const queue = await client.queue({ queueName: "orders" }).get();
console.log(queue.parallelism);
console.log(queue.lag); // Messages waiting
console.log(queue.paused);
// List all queues
const queues = await client.queue().list();
// Pause queue (stops processing new messages)
await client.queue({ queueName: "orders" }).pause();
// Resume queue
await client.queue({ queueName: "orders" }).resume();
// Delete queue
await client.queue({ queueName: "orders" }).delete();
```
### All Message Options Work
```typescript
await client.queue({ queueName: "orders" }).enqueueJSON({
url: "https://api.example.com/process",
body: { order: "data" },
delay: 60,
retries: 5,
timeout: 30,
callback: "https://api.example.com/callback",
deduplicationId: "order-123",
});
```
See [Publishing Messages](publishing-messages.md) for all options.
## Flow Control
Control message processing rate and concurrency without queues. More flexible than queue parallelism.
### Basic Flow Control
```typescript
await client.publishJSON({
url: "https://api.example.com/webhook",
body: { userId: "user-123", event: "action" },
flowControl: {
key: "user-123", // Group messages by key
parallelism: 2, // Max 2 concurrent requests for this key
rate: 10, // Max 10 requests
period: 60, // Per 60 seconds
},
});
```
### Flow Control Options
**key** (required): Groups messages for rate limiting
- Example: `user-${userId}`, `api-${service}`, `tenant-${tenantId}`
**parallelism** (optional): Max concurrent active requests with same key
- Example: `parallelism: 3` = max 3 requests in-flight
**rate** (optional): Max requests to activate within period
- Example: `rate: 100` with `period: 60` = 100 requests per minute
**period** (optional): Time window for rate limit in seconds or duration string
- Default: `1` (1 second)
- Examples: `60`, `"1m"`, `"5s"`, `"1h"`
## Queues vs Flow Control
**Use Queues when:**
- Need strict FIFO ordering
- Messages must process sequentially
- Single destination with controlled throughput
**Use Flow Control when:**
- Rate limiting by user, tenant, or other key
- Need flexible concurrency control
- No strict ordering required
- Works with any publish/schedule operation
## Best Practices
- Use descriptive queue names: `order-processing`, `email-sending`
- Use descriptive flow control keys: `user-${id}`, `tenant-${id}`
- Start with low parallelism and increase based on capacity
- Monitor queue lag to detect processing bottlenecks
- Use Flow Control for multi-tenant rate limiting
- Pause queues during maintenance, not deletion
- Use deduplication to prevent duplicate enqueues
```
### fundamentals/url-groups.md
```markdown
# URL Groups
URL Groups (also called Topics) let you publish a single message to multiple endpoints simultaneously.
## Creating URL Groups
Add endpoints to create or update a URL group:
```typescript
import { Client } from "@upstash/qstash";
const client = new Client({ token: process.env.QSTASH_TOKEN! });
await client.urlGroups().addEndpoints({
name: "payment-webhooks",
endpoints: [
{ url: "https://api1.example.com/webhook" },
{ url: "https://api2.example.com/webhook" },
{ url: "https://api3.example.com/webhook", name: "primary" },
],
});
```
- `name`: URL group identifier (alphanumeric, hyphens, underscores, periods)
- `url`: Endpoint URL (required)
- `name`: Optional endpoint name for identification
## Publishing to URL Groups
Publish once, deliver to all endpoints:
```typescript
const result = await client.publishJSON({
urlGroup: "payment-webhooks",
body: { orderId: "123", amount: 99.99, status: "paid" },
});
// Returns array with result for each endpoint
result.forEach((r) => {
console.log(`Sent to ${r.url}: ${r.messageId}`);
});
```
Each endpoint gets a separate message with its own retry logic and tracking.
## Managing Endpoints
### Remove Endpoints
Remove by URL or name:
```typescript
// Remove by URL
await client.urlGroups().removeEndpoints({
name: "payment-webhooks",
endpoints: [{ url: "https://api2.example.com/webhook" }],
});
// Remove by endpoint name
await client.urlGroups().removeEndpoints({
name: "payment-webhooks",
endpoints: [{ name: "primary" }],
});
```
### List URL Groups
```typescript
const groups = await client.urlGroups().list();
groups.forEach((group) => {
console.log(`${group.name}: ${group.endpoints.length} endpoints`);
group.endpoints.forEach((ep) => {
console.log(` - ${ep.url}${ep.name ? ` (${ep.name})` : ""}`);
});
});
```
### Get Specific URL Group
```typescript
const group = await client.urlGroups().get("payment-webhooks");
console.log(`Created: ${new Date(group.createdAt)}`);
console.log(`Updated: ${new Date(group.updatedAt)}`);
console.log(`Endpoints: ${group.endpoints.length}`);
```
### Delete URL Group
```typescript
await client.urlGroups().delete("payment-webhooks");
```
Deleting a URL group does not affect in-flight messages.
## URL Group vs Individual Publishing
**Use URL Groups when:**
- Broadcasting same message to multiple endpoints
- Need to manage endpoint list centrally
- Adding/removing endpoints dynamically
- All endpoints process the same data
**Use individual publishing when:**
- Each endpoint needs different message content
- Different retry/timeout settings per endpoint
- Endpoints have different purposes
## All Message Options Work
URL Groups support all publishing options:
```typescript
await client.publishJSON({
urlGroup: "notifications",
body: { event: "user.signup", userId: "123" },
delay: 60,
retries: 5,
callback: "https://api.example.com/callback",
deduplicationId: "signup-123",
});
```
See [Publishing Messages](publishing-messages.md) for all options.
```
### verification/receiver.md
```markdown
# Receiver - Message Verification
## Overview
The `Receiver` class verifies that incoming requests are genuinely from QStash by validating JWT signatures. This prevents unauthorized requests from reaching your endpoints.
## Getting Your Signing Keys
Sign in to the [Upstash Console](https://console.upstash.com/qstash) and navigate to your QStash instance to find:
- **Current Signing Key**: Active key for signature verification
- **Next Signing Key**: Key to use after rotation
Store these as environment variables:
```bash
QSTASH_CURRENT_SIGNING_KEY="your_current_key"
QSTASH_NEXT_SIGNING_KEY="your_next_key"
```
## Creating a Receiver Instance
### Basic Setup
```typescript
import { Receiver } from "@upstash/qstash";
const receiver = new Receiver({
currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
});
```
### Multi-Region Mode
If you're using multi-region QStash (with `QSTASH_REGION` environment variable set), signature verification requires additional configuration. The SDK automatically detects the region from the `upstash-region` header and uses region-specific signing keys.
> **Important:** Multi-region signature verification requires careful setup. See [Multi-Region Setup](../advanced/multi-region.md) for complete details on environment variables, region detection, and verification strategies.
## Verifying Incoming Requests
### Basic Verification
```typescript
try {
await receiver.verify({
signature: request.headers.get("upstash-signature")!,
body: await request.text(),
});
// Request is valid - process it
return new Response("OK", { status: 200 });
} catch (error) {
// Invalid signature
return new Response("Unauthorized", { status: 401 });
}
```
### With URL Verification
For extra security, verify the request was sent to the correct URL:
```typescript
await receiver.verify({
signature: request.headers.get("upstash-signature")!,
body: await request.text(),
url: "https://my-api.example.com/webhook",
});
```
### With Clock Tolerance
Handle minor clock differences between servers:
```typescript
await receiver.verify({
signature: request.headers.get("upstash-signature")!,
body: await request.text(),
clockTolerance: 5, // Allow 5 seconds difference
});
```
## Required Headers
QStash sends these headers with every request:
- `Upstash-Signature`: JWT signature to verify
> **Note:** In multi-region mode, QStash also sends an `Upstash-Region` header. See [Multi-Region Setup](../advanced/multi-region.md) for details.
## Handling Verification Failures
### SignatureError
The `verify()` method throws `SignatureError` for invalid signatures:
```typescript
import { Receiver, SignatureError } from "@upstash/qstash";
try {
await receiver.verify({
signature: request.headers.get("upstash-signature")!,
body: await request.text(),
});
} catch (error) {
if (error instanceof SignatureError) {
console.error("Invalid signature:", error.message);
return new Response("Invalid signature", { status: 401 });
}
throw error;
}
```
### Common Failure Reasons
1. **Missing or wrong signing keys**
- Verify keys in Upstash Console match environment variables
2. **Body mismatch**
- Ensure you pass the raw request body (not parsed JSON)
- Don't modify the body before verification
3. **Expired signature**
- QStash signatures expire after 5 minutes
- Check server clock is synchronized
- Use `clockTolerance` if needed
4. **URL mismatch**
- Ensure the `url` parameter matches the destination URL
- Include protocol, domain, and path
## Key Rotation
The Receiver supports seamless key rotation:
1. Verification tries `currentSigningKey` first
2. If that fails, tries `nextSigningKey`
3. Only throws error if both fail
To rotate keys:
1. Set new key as `QSTASH_NEXT_SIGNING_KEY`
2. Wait for all in-flight requests to complete
3. Update `QSTASH_CURRENT_SIGNING_KEY` to the new key
4. Generate a new `QSTASH_NEXT_SIGNING_KEY`
## Best Practices
### Always Verify
Never trust incoming requests without verification:
```typescript
// ❌ Don't do this
app.post("/webhook", async (req) => {
const data = await req.json();
processWebhook(data); // Unverified!
});
// ✅ Do this
app.post("/webhook", async (req) => {
const body = await req.text();
await receiver.verify({
signature: req.headers.get("upstash-signature")!,
body,
});
const data = JSON.parse(body);
processWebhook(data);
});
```
### Use Environment Variables
Never hardcode signing keys:
```typescript
// ❌ Don't do this
const receiver = new Receiver({
currentSigningKey: "sig_abc123...",
});
// ✅ Do this
const receiver = new Receiver({
currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
});
```
### Read Body Once
Request bodies can only be read once. Save it for reuse:
```typescript
const body = await request.text();
// Verify with raw body
await receiver.verify({
signature: request.headers.get("upstash-signature")!,
body,
});
// Parse after verification
const data = JSON.parse(body);
```
## Platform-Specific Verification
For framework-specific implementations, see:
- [Next.js Verification](platform-specific/nextjs.md)
```
### verification/platform-specific/nextjs.md
```markdown
# Next.js Endpoint Verification
## Overview
Next.js applications can use QStash in both App Router (route handlers) and Pages Router (API routes). The SDK provides dedicated verification utilities for each.
## App Router Verification
### Using verifySignatureAppRouter
The SDK provides `verifySignatureAppRouter` for App Router route handlers:
```typescript
import { verifySignatureAppRouter } from "@upstash/qstash/nextjs";
// app/api/webhook/route.ts
export const POST = verifySignatureAppRouter(async (req) => {
const body = await req.json();
// Request is verified - process it
console.log("Received verified message:", body);
return new Response("OK", { status: 200 });
});
```
### With Custom Configuration
```typescript
import { verifySignatureAppRouter } from "@upstash/qstash/nextjs";
export const POST = verifySignatureAppRouter(
async (req) => {
const body = await req.json();
return Response.json({ received: true });
},
{
currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY,
nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY,
clockTolerance: 5, // Allow 5 seconds clock difference
}
);
```
### Multi-Region Support
```typescript
import { verifySignatureAppRouter } from "@upstash/qstash/nextjs";
export const POST = verifySignatureAppRouter(async (req) => {
const upstashRegion = req.headers.get("upstash-region");
console.log("Request from region:", upstashRegion);
const body = await req.json();
return Response.json({ region: upstashRegion, data: body });
});
```
## Pages Router Verification
### Using verifySignature
For Pages Router API routes, use the `verifySignature` wrapper:
```typescript
import type { NextApiRequest, NextApiResponse } from "next";
import { verifySignature } from "@upstash/qstash/nextjs";
// pages/api/webhook.ts
async function handler(req: NextApiRequest, res: NextApiResponse) {
const body = req.body;
// Request is verified
console.log("Received:", body);
res.status(200).json({ success: true });
}
export default verifySignature(handler);
```
### With Configuration
```typescript
import type { NextApiRequest, NextApiResponse } from "next";
import { verifySignature } from "@upstash/qstash/nextjs";
async function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ message: "Verified" });
}
export default verifySignature(handler, {
currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY,
nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY,
clockTolerance: 5,
});
```
## Manual Verification with Receiver
For more control, use the `Receiver` class directly:
### Manual Verification
```typescript
import { Receiver } from "@upstash/qstash";
import { NextRequest, NextResponse } from "next/server";
const receiver = new Receiver({
currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
});
export async function POST(req: NextRequest) {
const signature = req.headers.get("upstash-signature");
if (!signature) {
return NextResponse.json({ error: "Missing signature" }, { status: 401 });
}
const body = await req.text();
try {
await receiver.verify({
signature,
body,
url: req.url,
});
// Verified - parse and process
const data = JSON.parse(body);
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
}
```
## Best Practices
### Handle Errors Gracefully
```typescript
export const POST = verifySignatureAppRouter(async (req) => {
try {
const body = await req.json();
await processWebhook(body);
return Response.json({ success: true });
} catch (error) {
console.error("Processing error:", error);
return Response.json({ error: "Processing failed" }, { status: 500 });
}
});
```
## Common Issues
### Missing Environment Variables
```typescript
if (!process.env.QSTASH_CURRENT_SIGNING_KEY) {
throw new Error("Missing QSTASH_CURRENT_SIGNING_KEY");
}
```
## Related Resources
- [General Receiver Documentation](../receiver.md)
- [Multi-Region Setup](../../advanced/multi-region.md)
- [Next.js Documentation](https://nextjs.org/docs)
- [Vercel Deployment](https://vercel.com/docs)
```
### advanced/callbacks.md
```markdown
# Callbacks
Callbacks let you receive delivery results without waiting for the HTTP request to complete. QStash calls your callback URL with the response after delivering the message.
## Why Use Callbacks?
Serverless functions have execution time limits. Callbacks allow you to:
- Publish long-running tasks without blocking
- Receive delivery confirmation asynchronously
- Handle failures separately with failure callbacks
You can use callbacks individually or together:
```typescript
await client.publishJSON({
url: "https://api.example.com/webhook",
body: { order: "12345" },
callback: "https://api.example.com/callback",
failureCallback: "https://api.example.com/failure",
});
```
## callback
Called after each delivery attempt (success or failure):
The callback is invoked after every retry attempt until the destination returns a 2XX status or retries are exhausted. Check `retried === maxRetries` in the callback body to detect final failure.
## failureCallback
Called only when all retries are exhausted:
Use this as a serverless alternative to polling the DLQ. See [DLQ](dlq.md) for more options.
## Callback Payload
### Success Callback Body
```json
{
"status": 200,
"header": { "content-type": ["application/json"] },
"body": "YmFzZTY0IGVuY29kZWQgcm9keQ==",
"retried": 2,
"maxRetries": 3,
"sourceMessageId": "msg_xxx",
"topicName": "myTopic",
"endpointName": "myEndpoint",
"url": "https://api.example.com/webhook",
"method": "POST",
"sourceHeader": { "content-type": "application/json" },
"sourceBody": "YmFzZTY0IGVuY29kZWQgcm9keQ==",
"notBefore": 1701198458025,
"createdAt": 1701198447054,
"scheduleId": "scd_xxx",
"callerIP": "178.247.74.179"
}
```
### Failure Callback Body
```json
{
"status": 500,
"header": { "content-type": ["text/plain"] },
"body": "RXJyb3IgbWVzc2FnZQ==",
"retried": 3,
"maxRetries": 3,
"dlqId": "1725323658779-0",
"sourceMessageId": "msg_xxx",
"topicName": "myTopic",
"endpointName": "myEndpoint",
"url": "https://api.example.com/webhook",
"method": "POST",
"sourceHeader": { "content-type": "application/json" },
"sourceBody": "YmFzZTY0IGVuY29kZWQgcm9keQ==",
"notBefore": 1701198458025,
"createdAt": 1701198447054,
"scheduleId": "scd_xxx",
"callerIP": "178.247.74.179"
}
```
### Field Descriptions
- `status` - HTTP status code from destination
- `header` - Response headers from destination
- `body` - Base64-encoded response body (may be truncated per plan limits)
- `retried` - Number of retry attempts made
- `maxRetries` - Maximum retry limit
- `dlqId` - Dead Letter Queue ID (failure callbacks only)
- `sourceMessageId` - Original message ID
- `topicName` - URL group name (if applicable)
- `endpointName` - Endpoint name within URL group (if applicable)
- `url` - Destination URL
- `method` - HTTP method used
- `sourceHeader` - Original message headers
- `sourceBody` - Base64-encoded original message body
- `notBefore` - Scheduled delivery time (Unix ms)
- `createdAt` - Message creation time (Unix ms)
- `scheduleId` - Schedule ID (if from schedule)
- `callerIP` - IP address that published the message
## Callback Configuration
Callbacks are themselves QStash messages and can be configured with the same options. Use the `Upstash-Callback-*` or `Upstash-Failure-Callback-*` header prefix:
**Not available via SDK parameters** - requires custom headers:
```typescript
await client.publish({
url: "https://api.example.com/webhook",
body: "data",
callback: "https://api.example.com/callback",
headers: {
// Configure callback behavior
"Upstash-Callback-Retries": "3",
"Upstash-Callback-Timeout": "30",
"Upstash-Callback-Method": "PUT",
"Upstash-Callback-Delay": "60",
// Forward custom headers to callback
"Upstash-Callback-Forward-Authorization": "Bearer token",
"Upstash-Callback-Forward-X-Custom": "value",
// Configure failure callback
"Upstash-Failure-Callback-Retries": "5",
"Upstash-Failure-Callback-Forward-Authorization": "Bearer token",
},
});
```
Available configuration headers:
- `Upstash-Callback-Retries` / `Upstash-Failure-Callback-Retries`
- `Upstash-Callback-Timeout` / `Upstash-Failure-Callback-Timeout`
- `Upstash-Callback-Delay` / `Upstash-Failure-Callback-Delay`
- `Upstash-Callback-Method` / `Upstash-Failure-Callback-Method`
- `Upstash-Callback-Forward-*` / `Upstash-Failure-Callback-Forward-*`
## Notes
- Callbacks are charged as regular messages
- Callbacks retry until the callback URL returns 2XX or retries are exhausted
- Response body may be truncated if it exceeds your plan's message size limit
- Both URLs must be publicly accessible
```
### advanced/dlq.md
```markdown
# Dead Letter Queue (DLQ)
Messages that fail after all retries are moved to the Dead Letter Queue for manual inspection and recovery.
## What is the DLQ?
When a message fails delivery after exhausting all retries, QStash moves it to the DLQ instead of discarding it. This lets you:
- Investigate failure reasons
- Manually retry after fixing issues
- Delete permanently failed messages
- Track patterns in failures
Common failure reasons:
- Destination endpoint errors (5XX responses)
- Timeouts
- Network issues
- Invalid responses from destination
## Listing DLQ Messages
```typescript
import { Client } from "@upstash/qstash";
const client = new Client({ token: process.env.QSTASH_TOKEN! });
const result = await client.dlq.listMessages();
console.log(`Found ${result.messages.length} failed messages`);
result.messages.forEach((msg) => {
console.log(`Message ${msg.messageId} to ${msg.url}`);
console.log(`Status: ${msg.responseStatus}`);
console.log(`DLQ ID: ${msg.dlqId}`);
});
```
## Pagination
Use cursor-based pagination for large DLQ:
```typescript
let cursor: string | undefined;
const allMessages = [];
do {
const result = await client.dlq.listMessages({
cursor,
count: 50, // Return up to 50 messages
});
allMessages.push(...result.messages);
cursor = result.cursor;
} while (cursor);
console.log(`Total failed messages: ${allMessages.length}`);
```
## Filtering DLQ Messages
Filter by various criteria:
```typescript
const result = await client.dlq.listMessages({
filter: {
messageId: "msg_123...",
url: "https://api.example.com/webhook",
urlGroup: "payment-webhooks",
queueName: "order-processing",
scheduleId: "scd_123...",
label: "payment-processing",
responseStatus: 500,
fromDate: oneDayAgo,
toDate: Date.now(),
callerIp: "192.168.1.1",
},
});
```
## Message Details
Each DLQ message includes:
```typescript
type DlqMessage = {
dlqId: string; // Unique DLQ identifier
messageId: string; // Original message ID
url: string; // Destination URL
method?: string; // HTTP method
header?: Record<string, string[]>; // Request headers
body?: string; // Request body
urlGroup?: string; // URL group name
queueName?: string; // Queue name
scheduleId?: string; // Schedule ID
createdAt: number; // Creation timestamp (ms)
notBefore?: number; // Scheduled delivery time (ms)
label?: string; // Message label
// Failure details
responseStatus?: number; // HTTP status from destination
responseHeader?: Record<string, string[]>; // Response headers
responseBody?: string; // Response body (UTF-8)
responseBodyBase64?: string; // Response body (base64 if non-UTF-8)
};
```
## Deleting Messages
```typescript
await client.dlq.delete("1725323658779-0");
await client.dlq.deleteMany({
dlqIds: ["1725323658779-0", "1725323658780-1", "1725323658781-2"],
});
```
## Understanding Failures
Inspect failure details:
```typescript
const result = await client.dlq.listMessages();
for (const msg of result.messages) {
console.log(`\nMessage ${msg.messageId}:`);
console.log(`URL: ${msg.url}`);
console.log(`Status: ${msg.responseStatus}`);
if (msg.responseBody) {
console.log(`Response: ${msg.responseBody}`);
} else if (msg.responseBodyBase64) {
const decoded = Buffer.from(msg.responseBodyBase64, "base64").toString();
console.log(`Response: ${decoded}`);
}
if (msg.responseHeader) {
console.log(`Headers:`, msg.responseHeader);
}
}
```
## Using Failure Callbacks
Instead of polling the DLQ, use failure callbacks for real-time notifications:
See [Callbacks](callbacks.md) for more details.
## DLQ Retention
Messages remain in the DLQ based on your plan:
- **Free**: 7 days
- **Paid**: Check your plan on [QStash Pricing](https://upstash.com/pricing/qstash)
Messages are automatically deleted when retention expires.
## Best Practices
- Set up failure callbacks for critical messages
- Regularly monitor DLQ for patterns
- Delete non-retriable messages to keep DLQ clean
- Use labels to categorize and filter failures
- Alert on DLQ message count thresholds
- Document common failure scenarios and resolutions
- Consider automated retries for known transient issues
```
### advanced/deduplication.md
```markdown
# Message Deduplication
Prevent duplicate message delivery within a 90-day window using deduplication IDs.
## Why Deduplication?
Duplicate messages can occur when:
- User retries a failed request
- Network issues cause message resubmission
- Application logic triggers multiple publishes for the same event
Deduplication ensures QStash accepts but doesn't enqueue duplicate messages.
## Deduplication Methods
### deduplicationId
Provide a custom identifier to detect duplicates:
```typescript
import { Client } from "@upstash/qstash";
const client = new Client({ token: process.env.QSTASH_TOKEN! });
await client.publishJSON({
url: "https://api.example.com/webhook",
deduplicationId: `order-${orderId}-payment`,
body: { orderId, status: "paid" },
});
```
If a message with the same `deduplicationId` was published in the last 90 days, the new message is accepted but not enqueued.
**Use cases:**
- Order processing: `order-${orderId}`
- User events: `user-${userId}-signup`
- Payment transactions: `payment-${transactionId}`
### contentBasedDeduplication
Automatically generate a deduplication ID from message content:
```typescript
await client.publishJSON({
url: "https://api.example.com/webhook",
contentBasedDeduplication: true,
body: { userId: "123", event: "signup" },
});
```
The hash includes:
- All headers (except authorization)
- Request body
- Destination URL
## Deduplication Window
Deduplication IDs are stored for **90 days**. After this period, a message with the same ID can be delivered again.
## Response Handling
When a duplicate is detected, the response includes the original message ID:
```typescript
const result = await client.publishJSON({
url: "https://api.example.com/webhook",
deduplicationId: "order-123",
body: { order: "data" },
});
if (result.deduplicated) {
console.log("Duplicate detected");
console.log("Original message ID:", result.messageId);
}
```
## Best Practices
- Use descriptive, deterministic deduplication IDs
- Include relevant context in custom IDs: `${entity}-${id}-${action}`
- Don't rely on deduplication for critical data consistency
- Monitor deduplicated messages in logs to detect issues
- Document your deduplication strategy for team reference
```