Rotor POSTs a signed JSON payload to your endpoint whenever a lifecycle event occurs. You register one or more webhook endpoints and choose which event types each one receives.

Event types

EventWhen it fires
job.completedA job finishes successfully
job.failedA job exhausts all retry attempts and enters terminal failure
job.stalledA job's lock expires while it was active (usually a crashed worker)
approval.pendingA job step is waiting for human approval
approval.approvedAn approval request was approved
approval.rejectedAn approval request was rejected
guardrail.blockedA guardrail rule blocked a job from executing
queue.drainedA queue's waiting count dropped to zero
queue.errorA queue-level error occurred (e.g. Redis unreachable)
webhook.testFired by rotor.webhooks.test() to verify your endpoint

Create a webhook endpoint

import { Rotor } from "@rotorsh/sdk";
 
const rotor = new Rotor({ apiKey: process.env.ROTOR_API_KEY! });
 
const webhook = await rotor.webhooks.create({
  url: "https://yourapp.com/webhooks/rotor",
  eventTypes: ["job.completed", "job.failed", "approval.pending"],
});
 
console.log(webhook.id);     // wh_<32hex>
console.log(webhook.secret); // shown ONCE — store it in your secrets manager immediately
Warning

The webhook secret is returned only at creation time. If you lose it, delete the webhook and create a new one. Store the secret in an environment variable (ROTOR_WEBHOOK_SECRET) before the creation response goes out of scope.

Verify signatures

Every request Rotor sends includes three headers:

HeaderContents
Rotor-Webhook-IdUnique delivery ID
Rotor-Webhook-TimestampUnix timestamp of the delivery
Rotor-Webhook-Signaturev1,{base64(HMAC-SHA256(secret, "{id}.{timestamp}.{body}"))}

Rotor uses a Svix-compatible HMAC-SHA256 scheme. Verify with the SDK helper:

import { verifyRotorSignature } from "@rotorsh/sdk";
import type { IncomingMessage, ServerResponse } from "http";
 
export async function POST(req: Request): Promise<Response> {
  const body = await req.text();
 
  const id        = req.headers.get("rotor-webhook-id")!;
  const timestamp = req.headers.get("rotor-webhook-timestamp")!;
  const signature = req.headers.get("rotor-webhook-signature")!;
 
  let event: unknown;
  try {
    event = verifyRotorSignature(body, signature, process.env.ROTOR_WEBHOOK_SECRET!, {
      id,
      timestamp,
      maxAgeSeconds: 300, // reject deliveries older than 5 minutes
    });
  } catch (err) {
    return new Response("Invalid signature", { status: 401 });
  }
 
  // event is now typed and verified — handle it
  await handleRotorEvent(event as RotorWebhookEvent);
 
  return new Response("OK", { status: 200 });
}
Tip

Always return 200 quickly — do your processing asynchronously or in a background task. If your endpoint takes longer than 10 seconds to respond, Rotor treats the delivery as a timeout and schedules a retry.

Handle specific events

job.completed

import type { RotorWebhookEvent } from "@rotorsh/sdk";
 
async function handleRotorEvent(event: RotorWebhookEvent) {
  switch (event.type) {
    case "job.completed": {
      const { jobId, queueName, result, completedAt } = event.data;
      console.log(`Job ${jobId} in ${queueName} finished:`, result);
      // update your DB, trigger downstream steps, etc.
      break;
    }
 
    case "job.failed": {
      const { jobId, queueName, failReason, attempts } = event.data;
      console.error(`Job ${jobId} failed after ${attempts} attempts: ${failReason}`);
      // send a Slack alert, open a support ticket, etc.
      break;
    }
 
    case "approval.pending": {
      const { jobId, approvalId, queueName, payload } = event.data;
      // notify a reviewer — e.g. post to Slack
      await notifyReviewer({ approvalId, payload });
      break;
    }
  }
}

Pipe job events to Slack

case "job.failed": {
  const { jobId, queueName, failReason } = event.data;
 
  await fetch(process.env.SLACK_WEBHOOK_URL!, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      text: `❌ Job \`${jobId}\` failed in \`${queueName}\`\n>${failReason}`,
    }),
  });
  break;
}

Route approval.pending to a bot

case "approval.pending": {
  const { approvalId, queueName, payload } = event.data;
 
  await fetch("https://yourapp.com/internal/approval-bot", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ approvalId, queueName, payload }),
  });
  break;
}

Test a webhook endpoint

Send a webhook.test event to confirm your endpoint is reachable and signature verification works:

await rotor.webhooks.test("wh_abc123def456...");

Your endpoint will receive a payload like:

{
  "type": "webhook.test",
  "webhookId": "wh_abc123def456...",
  "data": {
    "message": "This is a test event from Rotor.",
    "timestamp": "2026-05-20T10:00:00.000Z"
  }
}

Verify your endpoint returns 200 and that signature verification passes before relying on it in production.

Retry behavior

If your endpoint returns a non-2xx status code, times out, or is unreachable, Rotor retries with exponential backoff — up to 5 attempts over approximately 3 hours. After 5 failed attempts, the delivery is abandoned and marked as permanently failed.

AttemptApproximate delay
1Immediate
2~30 seconds
3~5 minutes
4~30 minutes
5~2 hours
Note

Delivery failures do not affect your job execution. Your jobs continue running — only the webhook notification is retried.

Manage endpoints

// List all webhook endpoints
const webhooks = await rotor.webhooks.list();
 
// Get a specific endpoint
const webhook = await rotor.webhooks.get("wh_abc123def456...");
console.log(webhook.url, webhook.eventTypes, webhook.createdAt);
 
// Delete an endpoint
await rotor.webhooks.delete("wh_abc123def456...");

Deleting a webhook stops all future deliveries immediately. In-flight deliveries that have already started may still complete.

callback_status on schedule runs

When a schedule fires a job and that job has a callback configured, the callback_status field on the run record reflects the webhook delivery outcome:

ValueMeaning
pendingDelivery has not been attempted yet
2xxEndpoint responded with a success status
4xxEndpoint responded with a client error
5xxEndpoint responded with a server error
timeoutEndpoint did not respond within the timeout window
nullNo callback configured for this run