# Guide to Cursor Webhooks: Features and Best Practices

Cursor webhooks notify your application when a Cloud Agent finishes or errors. If you kick off background coding agents and need to react when they complete, open a pull request, or fail, webhooks are how you learn about it without polling.

This guide covers how Cursor Cloud Agent webhooks work, the `statusChange` event, how to verify the `X-Webhook-Signature`, and the best practices for production.

## What are Cursor webhooks?

Cursor Cloud Agent webhooks are JSON POSTs delivered to a URL you configure. Each is signed with an `X-Webhook-Signature` header formatted `sha256=<hex>`: an HMAC-SHA256 over the raw request body. Cursor also sends an `X-Webhook-Id` (a unique delivery ID) and an `X-Webhook-Event` (the event type, `statusChange`).

## Cursor webhook features

| Feature | Details |
| --- | --- |
| Configuration | Cursor dashboard > Cloud Agent settings > Webhooks |
| Signature header | `X-Webhook-Signature`, formatted `sha256=<hex>` |
| Signature scheme | HMAC-SHA256 (hex) over the raw body, keyed with the webhook secret |
| Extra headers | `X-Webhook-Id` (delivery ID), `X-Webhook-Event` (`statusChange`) |
| Payload | `event`, `timestamp`, `id`, `status`, `source`, `target`, `summary` |
| SDK | None; verify manually |

## Common events

Cursor has a single event type, `statusChange`, whose `status` field carries the outcome:

| `status` | Fires when |
| --- | --- |
| `FINISHED` | The agent completed successfully |
| `ERROR` | The agent encountered an error |

Branch on the `status` field, not on separate event names, and read `target.prUrl` / `target.branchName` when an agent opens a pull request.

## Setting up Cursor webhooks

In the Cursor dashboard > Cloud Agent settings > Webhooks, add your endpoint URL, select the `statusChange` event, and copy the webhook signing secret into `CURSOR_WEBHOOK_SECRET`. On setup, Cursor sends a test `statusChange` event, verify it and return 200.

## Securing Cursor webhooks

Split the `X-Webhook-Signature` on `=`, confirm the algorithm is `sha256`, compute an HMAC-SHA256 over the raw body, hex-encode it, and compare in constant time. Verify against the raw body before parsing.

```javascript
const crypto = require("crypto");

const SECRET = process.env.CURSOR_WEBHOOK_SECRET;

function verify(rawBody, signatureHeader) {
  if (!signatureHeader) return false;
  const parts = signatureHeader.split("=");
  if (parts.length !== 2 || parts[0] !== "sha256") return false;

  const expected = crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
  const a = Buffer.from(parts[1]);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhooks/cursor", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.headers["x-webhook-signature"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge fast
  const payload = JSON.parse(req.body.toString());
  processQueue.add(payload); // branch on payload.status, dedupe on X-Webhook-Id, async
});

```

The same check in Python:

```python
import hashlib
import hmac
import os

SECRET = os.environ["CURSOR_WEBHOOK_SECRET"].encode()

def verify(raw_body: bytes, signature_header: str) -> bool:
    if not signature_header:
        return False
    parts = signature_header.split("=")
    if len(parts) != 2 or parts[0] != "sha256":
        return False
    expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(parts[1], expected)

```

## Cursor webhook limitations and pain points

### One event type, status in the payload

The Problem: There's only `statusChange`. Code that expects distinct `FINISHED` or `ERROR` events never fires, because those are `status` field values.

Why It Happens: Cursor models outcomes as a status field on a single event.

Workarounds:

* Branch on `payload.status` (`FINISHED` / `ERROR`), and read `target` for PR details.

How Hookdeck Can Help: Hookdeck's filters can route on the `status` field, giving you outcome-based routing.

### Verify over the raw body

The Problem: The HMAC is over the exact bytes Cursor sent. Parsing and re-serializing the JSON changes the bytes and breaks verification.

Why It Happens: JSON re-serialization reorders keys and whitespace.

Workarounds:

* Capture the raw body (`express.raw`) and verify before parsing.

How Hookdeck Can Help: Hookdeck verifies against the received bytes at the edge.

### Dedupe on X-Webhook-Id

The Problem: Retried deliveries repeat the same event, and the signature proves authenticity, not uniqueness.

Why It Happens: At-least-once delivery.

Workarounds:

* Dedupe on the `X-Webhook-Id` delivery ID and make handlers idempotent.

How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### A single, fresh topic

The Problem: Cloud Agent webhooks are new, so patterns and tooling are still thin, and there's no official SDK verification helper.

Why It Happens: It's a recent feature.

Workarounds:

* Verify manually with the HMAC-SHA256 check above.

How Hookdeck Can Help: Hookdeck gives you verification, observability, and retries at the edge, so a young webhook source is production-ready without waiting for an SDK.

## Best practices

### Verify HMAC-SHA256 over the raw body

Confirm the `sha256=` prefix, compute the hex HMAC over the raw body, and compare in constant time.

### Branch on the status field

Handle `FINISHED` and `ERROR` from `payload.status`, and read `target` for PR and branch details.

### Dedupe on X-Webhook-Id

Persist the delivery ID and skip repeats.

### Acknowledge fast, process asynchronously

Return 200 quickly and defer work to a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Cursor Cloud Agent webhooks are verified with an `X-Webhook-Signature` HMAC-SHA256 over the raw body, formatted `sha256=<hex>`, alongside `X-Webhook-Id` and `X-Webhook-Event` headers. There's one event, `statusChange`, whose `status` is `FINISHED` or `ERROR`. Verify over the raw body, branch on the status, and dedupe on the delivery ID.

[Hookdeck](https://hookdeck.com) verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Cursor webhooks reliably in minutes.