# Guide to Onshape Webhooks: Features and Best Practices

Onshape webhooks tell your application when something changes in a CAD document: a part studio is edited, a version or revision is created, a release moves through a workflow, a translation finishes, a comment is posted, or a user is added to the company. If you build PLM, ERP, manufacturing or review tools on top of Onshape, webhooks let you react to those changes instead of polling the REST API.

This guide covers how Onshape webhooks are registered and validated, how signature keys work, and how to build a receiver that stays registered and verifies every notification.

## What are Onshape webhooks?

An Onshape webhook is a registration your application creates with the REST API. It names the events you want, the scope they apply to (a company, a document or an OAuth client) and the URL to notify. Onshape then sends each notification as an HTTP `POST` with a JSON body.

Signing is a separate, Enterprise company setting. Once an administrator generates signature keys, every notification carries `X-onshape-webhook-timestamp` and one signature header per key: `X-onshape-webhook-signature-primary` and, if a secondary key exists, `X-onshape-webhook-signature-secondary`. Each signature is the base64 HMAC-SHA256 of the timestamp value, a period and the raw body.

## Onshape webhook features

| Feature | Details |
| --- | --- |
| Configuration | REST API: `POST /api/v6/webhooks` on `cad.onshape.com`, or your company domain for Enterprise |
| Scope | `companyId`, `documentId` or `clientId`, depending on the event |
| Registration check | A `webhook.register` trial that must return HTTP `200` |
| Cleanup | Webhooks are removed after inactivity unless `isTransient` is `false` |
| Endpoint URL | HTTP or HTTPS. HTTPS needs a certificate from a CA Onshape recognises |
| Signature headers | `X-onshape-webhook-signature-primary` and `-secondary`, standard base64 |
| Timestamp header | `X-onshape-webhook-timestamp`, when the webhook was sent (format not documented) |
| Signed content | `<timestamp>.<raw body>` |
| Secrets | Primary and secondary keys, set by an administrator in company settings |
| Basic auth | Optional, fills the `Authorization` header |
| Event field | `event` in the body |
| Message ID | `messageId` in the body |
| Retries | Not documented |

## Common events

The full list lives in the API Explorer, under **createWebhook > Callbacks**. These are the ones most integrations start with:

| Event | Fires when |
| --- | --- |
| `onshape.model.lifecycle.changed` | A change is made to a model |
| `onshape.model.lifecycle.createversion` | A new version of a document is created |
| `onshape.model.lifecycle.metadata` | Part or element metadata is modified |
| `onshape.model.translation.complete` | A translation request completes |
| `onshape.document.lifecycle.created` | A document is created |
| `onshape.document.lifecycle.statechange` | A document moves to `ACTIVE`, `TRASH` or `DELETED` |
| `onshape.revision.created` | A revision is created |
| `onshape.workflow.transition` | A revision or release package moves through workflow states |
| `onshape.comment.create`, `.update`, `.delete` | A comment is created, updated or deleted |
| `onshape.task.created`, `.updated`, `.deleted`, `.transition` | A task changes |
| `onshape.team.addmember`, `.removemember`, `.created`, `.deleted` | A team changes |
| `onshape.user.lifecycle.updateappsettings` | A user's application settings change |
| `webhook.register`, `webhook.ping`, `webhook.unregister` | Sent automatically when a webhook is registered, pinged or removed |

Most events are registered against a `companyId`, which covers all present and future documents the company owns. `onshape.document.lifecycle.statechange` needs a `documentId` and a `documentState`, and `onshape.user.lifecycle.updateappsettings` needs your OAuth app's `clientId`.

Notifications share one shape. Here is Onshape's `onshape.document.lifecycle.statechange` example:

```json
{
  "timestamp": "2024-05-05T23:46:29.284-0500",
  "event": "onshape.document.lifecycle.statechange",
  "workspaceId": "000000000000000000000000",
  "elementId": "000000000000000000000000",
  "webhookId": "000000000000000000000000",
  "messageId": "000000000000000000000000",
  "data": "Some data",
  "documentId": "000000000000000000000000",
  "versionId": "000000000000000000000000",
  "documentState": "TRASH"
}

```

Notifications carry IDs rather than the changed data, so most handlers call the REST API to read the document, element or revision they're told about.

## Setting up Onshape webhooks

1. Make your endpoint return HTTP `200` before you register anything. The `webhook.register` trial can arrive before the registration call returns.
2. As a company administrator, click your account icon, then **Company/Classroom/Enterprise settings > Webhooks**. Under **Webhook signatures**, generate a primary key, click **Save changes** and copy the key to your server, for example as `ONSHAPE_WEBHOOK_SECRET`.
3. Register the webhook:

```bash
curl -X POST "https://cad.onshape.com/api/v6/webhooks" \
  -H "Authorization: Basic CREDENTIALS" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "000000000000000000000000",
    "events": ["onshape.model.lifecycle.createversion"],
    "options": { "collapseEvents": false },
    "isTransient": false,
    "url": "https://your-endpoint.example.com/webhooks/onshape"
  }'

```

1. Confirm your endpoint received `webhook.register`. Call `POST /api/v6/webhooks/{webhookId}/ping` at any time to send a `webhook.ping`.

Get the `companyId` from `findCompany` (the `id` field, not `address.id` or `ownerId`), or pass a `documentId` the company owns and let Onshape infer it.

If you receive Onshape webhooks through Hookdeck, use the Hookdeck source URL as the webhook `url` and create a source with the Onshape type. The source asks you to:

> Onshape signature keys are an Enterprise company setting. Paste the primary key; only the primary signature is verified.

The Onshape source accepts `POST` only, which is how Onshape delivers notifications. Hookdeck returns HTTP `200` to the `webhook.register` trial, so registration completes. Onshape doesn't say whether the trial is signed. If it isn't, Hookdeck records it as failed verification, which doesn't affect the registration.

## Securing Onshape webhooks

Read `X-onshape-webhook-timestamp` and `X-onshape-webhook-signature-primary`. Build the message from the timestamp value exactly as received, a period and the raw body bytes. Compute an HMAC-SHA256 with the primary key, base64-encode it, and compare it with the header in constant time. Base64 is case-sensitive, so compare it exactly.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyOnshapeSignature(rawBody, signatureHeader, timestampHeader, primaryKey) {
  if (!signatureHeader || !timestampHeader || !primaryKey) return false;

  const message = Buffer.concat([Buffer.from(timestampHeader, 'latin1'), Buffer.from('.'), rawBody]);
  const expected = crypto.createHmac('sha256', primaryKey).update(message).digest('base64');

  const received = Buffer.from(signatureHeader, 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/onshape', express.raw({ type: '*/*' }), (req, res) => {
  const primaryKey = process.env.ONSHAPE_WEBHOOK_SECRET;
  if (!primaryKey) return res.sendStatus(500); // fail closed

  const valid = verifyOnshapeSignature(
    req.body,
    req.get('x-onshape-webhook-signature-primary'),
    req.get('x-onshape-webhook-timestamp'),
    primaryKey
  );
  if (!valid) return res.sendStatus(401);

  const notification = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on notification.messageId, route on notification.event, then process asynchronously.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import hashlib
import hmac

def verify_onshape_signature(raw_body: bytes, signature_header, timestamp_header, primary_key: str) -> bool:
    if not signature_header or not timestamp_header or not primary_key:
        return False
    message = timestamp_header.encode("latin-1") + b"." + raw_body
    expected = base64.b64encode(hmac.new(primary_key.encode("utf-8"), message, hashlib.sha256).digest())
    return hmac.compare_digest(signature_header.encode("utf-8"), expected)

# In FastAPI: raw_body = await request.body()
# verify_onshape_signature(raw_body, request.headers.get("x-onshape-webhook-signature-primary"),
#                          request.headers.get("x-onshape-webhook-timestamp"),
#                          os.environ["ONSHAPE_WEBHOOK_SECRET"])

```

Onshape's own Node sample compares with `===`, which isn't constant-time, and its Java sample signs `messageBody.getBytes()`, which uses the platform's default charset. Hashing the raw bytes avoids both problems.

Onshape documents no timestamp tolerance and no format for `X-onshape-webhook-timestamp`, so the code doesn't check its age. Hookdeck applies no timestamp tolerance either, and its retries and replays resend the original headers. If you add a window, confirm the header's format from a real notification first.

To check your verifier, use the key `onshape-guide-primary-key`, an `X-onshape-webhook-timestamp` of `1761480000000` (an illustrative value, since the format isn't documented) and this exact body:

```
{"event":"webhook.ping","webhookId":"000000000000000000000000","messageId":"000000000000000000000000"}

```

The header should be:

```
X-onshape-webhook-signature-primary: q32GmDmVgQ1OtfvL7KLHSwZ9gQdlxLXejSSQo6tRyxU=

```

> **Make Onshape webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) answers the `webhook.register` trial with a `200`, verifies the primary signature on every notification, and queues each CAD event until your service has processed it.

## Onshape webhook limitations and pain points

### Signing needs an Enterprise administrator

**The Problem:** Notifications arrive with no signature headers.

**Why It Happens:** Signature keys are a company setting, and the alert on Hookdeck's Onshape source describes them as an Enterprise setting. A webhook registered by an app in an account without keys is delivered unsigned.

**Workarounds:**

* Ask a company administrator to generate a primary key under **Company/Classroom/Enterprise settings > Webhooks**.
* Where signing isn't available, turn on webhook basic authentication, which fills the `Authorization` header, and check it on your endpoint.
* Re-read anything important from the REST API, which needs your own credentials.

**How Hookdeck Can Help:** Hookdeck's Onshape source verifies the primary signature. Unsigned notifications are marked as failed verification in the request log, so you can see straight away that keys aren't set.

### Rotation with two keys, verified on one header

**The Problem:** Verification fails after an administrator swaps keys.

**Why It Happens:** Onshape signs with each key into its own header and says that "If either key matches, it will be accepted as valid by Onshape." Hookdeck's Onshape source, and the code above, check the primary header only. If the key you hold is now in the secondary slot, the primary signature no longer matches.

**Workarounds:**

* During rotation, always paste the key that's currently in the primary slot.
* If you verify on your own server, check the secondary header with the secondary key as a fallback while you rotate.

**How Hookdeck Can Help:** Update the source's key at the moment the primary key changes. Notifications that arrive in between are kept in the request log, marked as failed verification, so you know which ones to check.

### The registration trial and silent cleanup

**The Problem:** A webhook registration disappears, either straight away or weeks later.

**Why It Happens:** If the `webhook.register` trial doesn't get HTTP `200`, Onshape cancels the registration. The trial can arrive before your registration call returns. Webhooks created without `isTransient=false` are cleaned up after a period of inactivity.

**Workarounds:**

* Deploy and test the endpoint before registering, and return `200` for `webhook.*` events.
* Set `isTransient=false` and unregister webhooks you no longer need.
* List your webhooks periodically with `GET /api/v6/webhooks` and re-register any that are missing.

**How Hookdeck Can Help:** A Hookdeck source answers every request with `200`, so a deploy on your side can't fail the trial.

### No documented retries

**The Problem:** Notifications sent during an outage may be lost, and a webhook can stop delivering.

**Why It Happens:** Onshape doesn't publish a retry policy. Its docs say that it keeps delivering until the application "either returns an error in response to a notification for the webhook or fails to respond at all for an extended period of time."

**Workarounds:**

* Return `200` quickly and do the work asynchronously.
* Use the REST API to catch up after downtime, for example by listing versions or revisions created since the last event you processed.

**How Hookdeck Can Help:** Hookdeck acknowledges each notification as soon as it arrives and stores it, then delivers to your service with [retries](/docs/retries) you configure, so your errors aren't what Onshape sees.

## Best practices

### Verify against the raw body

Onshape signs the payload exactly as sent. In Express, use `express.raw({ type: '*/*' })`. In Next.js, use `Buffer.from(await request.arrayBuffer())`. In FastAPI, use `await request.body()`.

### Deduplicate on `messageId`

Each notification has a `messageId`. Record the IDs you've processed and skip repeats. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency) and Hookdeck's [deduplication](/docs/deduplication).

### Use HTTPS

Onshape accepts HTTP URLs, but its help page tells administrators to send webhooks to HTTPS endpoints because the data isn't encrypted otherwise. Use a certificate from a recognised CA, since self-signed certificates make delivery fail.

### Expect high volume from model events

`onshape.model.lifecycle.changed` fires when a change is made to a model, so it may arrive often while someone is editing. Register only the events you need, and consider `createversion` or `revision.created` when you only care about milestones.

## Conclusion

Onshape webhooks are registered by API, validated with a `webhook.register` trial, and signed only when an Enterprise administrator has set keys. Sign the timestamp, a period and the raw body with the primary key, keep the registration alive with `isTransient=false`, and plan for catch-up through the REST API because retries aren't documented.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks every Onshape signature, and holds each notification until your service has processed it.

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