project is created.
Example
Webhook for Project Creation
Create a new project and receive a webhook notification.
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.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.| 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 ofX-Opus-Signature like this:
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.
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.Verifying a webhook on your server
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 2: Recompute the signature
Calculate
HMAC-SHA256(secretKey, body + salt) using your shared secret and the salt
from the header.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 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.Example (Node.js)
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.