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 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:
QSTASH_CURRENT_SIGNING_KEY="your_current_key"
QSTASH_NEXT_SIGNING_KEY="your_next_key"Creating a Receiver Instance
Basic Setup
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 for complete details on environment variables, region detection, and verification strategies.
Verifying Incoming Requests
Basic Verification
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:
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:
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-Regionheader. See Multi-Region Setup for details.
Handling Verification Failures
SignatureError
The verify() method throws SignatureError for invalid signatures:
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
Missing or wrong signing keys
- Verify keys in Upstash Console match environment variables
Body mismatch
- Ensure you pass the raw request body (not parsed JSON)
- Don't modify the body before verification
Expired signature
- QStash signatures expire after 5 minutes
- Check server clock is synchronized
- Use
clockToleranceif needed
URL mismatch
- Ensure the
urlparameter matches the destination URL - Include protocol, domain, and path
- Ensure the
Key Rotation
The Receiver supports seamless key rotation:
- Verification tries
currentSigningKeyfirst - If that fails, tries
nextSigningKey - Only throws error if both fail
To rotate keys:
- Set new key as
QSTASH_NEXT_SIGNING_KEY - Wait for all in-flight requests to complete
- Update
QSTASH_CURRENT_SIGNING_KEYto the new key - Generate a new
QSTASH_NEXT_SIGNING_KEY
Best Practices
Always Verify
Never trust incoming requests without verification:
// ❌ 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:
// ❌ 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:
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: