> ## Documentation Index
> Fetch the complete documentation index at: https://help.opus.pro/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook

Webhooks are a way for your application to receive real-time notifications from the API when a specific event occurs.
For example, you can use webhooks to receive notifications when a new `project` is created.

## Example

<CardGroup cols={2}>
  <Card title="Webhook for Project Creation" icon="webhook" href="/api-reference/endpoints/create-project#conclusion-actions">
    Create a new project and receive a webhook notification.
  </Card>
</CardGroup>

## Securing your webhook

Every webhook we send is signed so that your server can confirm it genuinely came from Opus
and was not modified in transit. The signature is built from a **secret key** that only you and
Opus share — it is never sent in the request.

<Note>
  **Your secret key is your Opus API secret key** (the `sk-…` value from your API settings).
  Opus signs each webhook with your organization's API secret key, so verify with that same
  value. If your organization has multiple API keys, the **first key created** is the one used
  for signing — keep it active, and if you rotate keys, update the secret on your server to
  match.
</Note>

Each POST request carries three headers:

| Header             | What it is                                                                              | Why it's there                                                                                                                                   |
| ------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `X-Opus-Signature` | An HMAC-SHA256 hash of the request body combined with the salt, keyed with your secret. | **Authenticity & integrity** — only a party holding the secret can produce it, so a valid signature proves the payload is genuine and unaltered. |
| `X-Opus-Salt`      | 8 random bytes (hex), regenerated for every request.                                    | Mixed into the signed input so two identical payloads still produce different signatures.                                                        |
| `X-Opus-Timestamp` | The Unix time (in seconds) when the webhook was sent.                                   | Lets you reject obviously old deliveries. See the note on replay protection below.                                                               |

### How the signature is computed

We generate the value of `X-Opus-Signature` like this:

```
signature = HMAC-SHA256(secretKey, body + salt)
```

Where `secretKey` is your Opus API secret key (the `sk-…` value — see the note above), `body`
is the exact raw JSON request body, and `salt` is the value sent in `X-Opus-Salt`. Because the
salt is appended to the body before hashing, you must use the **raw body bytes** —
re-serializing the parsed JSON may change whitespace or key order and break the comparison.

<Note>
  The timestamp is **not** part of the signed input — only `body + salt` is hashed. The
  freshness check in Step 4 is therefore best-effort: it rejects stale deliveries, but a
  determined attacker who captures a request could replay it with a fresh timestamp. For
  strong replay protection, also record the `X-Opus-Salt` (or the signature) of each request
  you accept and reject any value you have already seen. See **Replay protection** below.
</Note>

### Verifying a webhook on your server

<Steps>
  <Step title="Step 1: Read the raw request body">
    Capture the request body exactly as received, before any JSON parsing. Also read the
    `X-Opus-Signature`, `X-Opus-Salt`, and `X-Opus-Timestamp` headers.
  </Step>

  <Step title="Step 2: Recompute the signature">
    Calculate `HMAC-SHA256(secretKey, body + salt)` using your shared secret and the salt
    from the header.
  </Step>

  <Step title="Step 3: Compare the signatures">
    Compare your computed value against `X-Opus-Signature` using a **constant-time**
    comparison. Decode both values to fixed-length buffers and check their lengths match
    *before* comparing — `crypto.timingSafeEqual` throws if the buffers differ in length, so
    a malformed or empty signature header must be rejected rather than allowed to crash your
    handler. If they don't match, reject the request.
  </Step>

  <Step title="Step 4: Check the timestamp (recommended)">
    Reject the request if `X-Opus-Timestamp` is outside an acceptable window (for example,
    more than 5 minutes from the current time) to drop obviously old deliveries.
  </Step>

  <Step title="Step 5: Reject replays (recommended)">
    Record the `X-Opus-Salt` of each request you accept (e.g. in a short-lived cache keyed by
    salt). If you see a salt you have already processed, reject the request as a replay.
  </Step>
</Steps>

### Example (Node.js)

```javascript theme={"dark"}
import crypto from 'crypto'

const seenSalts = new Set() // use a TTL cache (e.g. Redis) in production

function verifyWebhook(rawBody, headers, secretKey) {
  const salt = headers['x-opus-salt']
  const timestamp = parseInt(headers['x-opus-timestamp'], 10)
  const received = headers['x-opus-signature']

  if (!received || !salt || Number.isNaN(timestamp)) return false

  // Step 2: recompute the signature
  const expected = crypto
    .createHmac('sha256', secretKey)
    .update(rawBody + salt)
    .digest('hex')

  // Step 3: constant-time compare — guard length first so a malformed
  // signature is rejected instead of throwing a RangeError.
  const a = Buffer.from(received, 'hex')
  const b = Buffer.from(expected, 'hex')
  const signatureValid = a.length === b.length && crypto.timingSafeEqual(a, b)
  if (!signatureValid) return false

  // Step 4: reject stale requests (5-minute window)
  const fresh = Math.abs(Math.floor(Date.now() / 1000) - timestamp) < 300
  if (!fresh) return false

  // Step 5: reject replays
  if (seenSalts.has(salt)) return false
  seenSalts.add(salt)

  return true
}
```

<Note>
  Keep your secret key private and store it securely. Anyone with the key can forge valid
  signatures. If you suspect it has leaked, rotate it.
</Note>
