# Guide to Cin7 Core Webhooks: Features and Best Practices

Cin7 Core webhooks tell your systems when orders, purchases, products and stock change. A sale is created or invoiced, a shipment is authorised, a purchase order is received, a product is updated or available stock moves. If you run inventory and order management on Cin7 Core, formerly DEAR Inventory, webhooks are how those changes reach your storefront, warehouse, accounting and reporting systems without polling the API.

This guide covers how Cin7 Core webhooks are created and authenticated, the webhook types and their payloads, the retry schedule that ends in a deactivated webhook, and the practices that make a Cin7 Core receiver dependable.

## What are Cin7 Core webhooks?

Cin7 Core webhooks are part of its API and need the **Automation** module on your subscription. Each webhook has one `Type`, such as `Sale/Created` or `Product/Updated`, an `ExternalURL` to send to, and an authorisation setting. You create them through the `/webhooks` endpoint of the Cin7 Core API, and you can have at most five webhooks of the same type at once.

Cin7 Core does not sign webhook requests. Instead, each webhook carries a credential you choose. `ExternalAuthorizationType` can be `noauth`, `basicauth` with a username and password, or `bearerauth` with a token, and `ExternalHeaders` lets you add any extra headers as key and value pairs. Your receiver checks the credential on every request.

## Cin7 Core webhook features

| Feature | Details |
| --- | --- |
| Requirement | The Automation module |
| Configuration | API: `POST https://inventory.dearsystems.com/ExternalApi/v2/webhooks` |
| Subscription model | One `Type` per webhook, at most 5 webhooks per type |
| Format | JSON |
| Event field | `EventType` in the body, except for two types with array bodies |
| Authentication | `noauth`, `basicauth` or `bearerauth`, plus optional `ExternalHeaders` |
| Signature and timestamp | None |
| Retries | 6 attempts: 1 minute after the event, then 5, 10, 15, 20 and 25 minutes after the previous attempt |
| After the last failure | The webhook is deactivated |
| Method, timeout, source IPs | Not documented |

## Common events

Cin7 Core documents 32 webhook types:

| Area | Types |
| --- | --- |
| Sales | `Sale/Created`, `Sale/QuoteAuthorised`, `Sale/OrderAuthorised`, `Sale/Backordered`, `Sale/PickAuthorised`, `Sale/PackAuthorised`, `Sale/ShipmentAuthorised`, `Sale/InvoiceAuthorised`, `Sale/CreditNoteAuthorised`, `Sale/Voided`, `Sale/Undo` |
| Sale payments and details | `Sale/PartialPaymentReceived`, `Sale/FullPaymentReceived`, `Sale/AttachmentAdded`, `Sale/AdditionalAttributesChanged`, `Sale/ShipmentTrackingNumberChanged` |
| Purchases | `Purchase/OrderAuthorised`, `Purchase/InvoiceAuthorised`, `Purchase/StockReceivedAuthorised`, `Purchase/CreditNoteAuthorised`, `Purchase/Updated` |
| Contacts and products | `Customer/Updated`, `Supplier/Updated`, `Product/Updated` |
| Stock | `Stock/AvailableStockLevelChanged` |
| CRM | `Lead/Updated`, `Lead/Converted`, `Opportunity/Authorized`, `Opportunity/AttachmentAdded`, `Opportunity/Voided`, `Opportunity/Converted` |
| Tasks | `Task/Overdue` |

Note the spelling: sales and purchases use `Authorised`, but opportunities use `Authorized`. There is no `Purchase/Created` type.

Most payloads are flat JSON objects with the webhook's type in `EventType`. Here is the `Sale/OrderAuthorised` example:

```json
{
  "SaleID": "91EE7B1D-BD35-4E43-B98A-DB86BE777624",
  "SaleOrderNumber": "SO-00044",
  "EventType": "Sale/OrderAuthorised"
}

```

Some types carry much more. `Sale/Created` adds customer and contact fields, and `Product/Updated` sends a `ProductDetailsList` with full product records. Two types are different: `Stock/AvailableStockLevelChanged` and `Task/Overdue` are documented as JSON arrays with no `EventType` at all, so the only way to know what they are is the webhook that sent them.

> **Inspect Cin7 Core webhooks as they arrive.** Use a [Hookdeck Inspect](https://inspect.hookdeck.com) URL as a webhook's `ExternalURL` to see the exact method, headers and body Cin7 Core sends, with no account or setup required.

## Setting up Cin7 Core webhooks

Generate a long random token first, for example with `openssl rand -hex 32`. Then create one webhook per type with the Cin7 Core API, using your account ID and application key:

```bash
curl -X POST https://inventory.dearsystems.com/ExternalApi/v2/webhooks \
  -H "api-auth-accountid: YOUR_ACCOUNT_ID" \
  -H "api-auth-applicationkey: YOUR_APPLICATION_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "Type": "Sale/OrderAuthorised",
    "IsActive": true,
    "ExternalURL": "https://example.com/webhooks/cin7-core/sale-order-authorised",
    "ExternalAuthorizationType": "bearerauth",
    "ExternalBearerToken": "YOUR_LONG_RANDOM_TOKEN"
  }'

```

Instead of `bearerauth`, you can set `noauth` and add your own header through `ExternalHeaders`, for example a key of `X-Webhook-Secret` with your token as the value.

To route through Hookdeck, create a Hookdeck source with the **Cin7 Core** type and use its URL as the `ExternalURL`. For `bearerauth`, enter `authorization` in the source's **API Key Header** field and `Bearer <token>` in its **API Key** field. For a custom header, enter its name and exact value. Hookdeck compares the whole value. The source covers bearer tokens and custom headers, not `basicauth`. It accepts `POST`.

Cin7 Core's documentation does not say which HTTP method webhooks use, or spell out the header `bearerauth` produces. `Authorization: Bearer <token>` is the usual form, so check your first delivery in Hookdeck to confirm the method and header before you rely on them.

## Securing Cin7 Core webhooks

Read the header you configured, compare its whole value with the one you stored in constant time, and only then parse the body. Do not strip a `Bearer ` prefix: store the full value instead.

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

// "Authorization" for bearerauth, or your own ExternalHeaders key.
const HEADER = (process.env.CIN7_CORE_WEBHOOK_HEADER || 'Authorization').toLowerCase();
const SECRET = process.env.CIN7_CORE_WEBHOOK_SECRET; // e.g. "Bearer 3f9c..."

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/cin7-core/:type', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.headers[HEADER];
  if (!SECRET || typeof received !== 'string' || !safeEqual(received, SECRET)) {
    return res.sendStatus(401);
  }

  let payload;
  try {
    payload = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Most bodies carry payload.EventType. Arrays (stock levels, overdue tasks) don't,
  // so fall back to the route's :type.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

def verify_cin7_core_request(headers) -> bool:
    secret = os.environ.get("CIN7_CORE_WEBHOOK_SECRET")
    if not secret:
        return False  # never accept when unconfigured
    name = os.environ.get("CIN7_CORE_WEBHOOK_HEADER", "Authorization")
    received = headers.get(name)  # case-insensitive in Starlette and Flask
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))

```

A static token authenticates the sender, not the body. It cannot detect a modified request, and with no signature or timestamp a captured request can be replayed. Use an HTTPS `ExternalURL` and a long random token, and update the webhook with a new token if the old one might have leaked. A webhook set to `noauth` with no extra headers carries nothing to check.

> **Make Cin7 Core webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks your token on every Cin7 Core webhook, acknowledges straight away so the six attempts are never used up, and queues every order and stock change durably.

## Cin7 Core webhook limitations and pain points

### Six failures deactivate the webhook

**The Problem:** After an outage of a little over an hour, Cin7 Core stops sending a webhook type entirely.

**Why It Happens:** Cin7 Core makes six attempts: one minute after the event, then 5, 10, 15, 20 and 25 minutes after each previous attempt. If all six fail, it deactivates the webhook, and nothing more arrives until someone switches it back on.

**Workarounds:**

* Return `200` as soon as the event is stored, and do the work afterwards.
* List your webhooks with `GET /webhooks` on a schedule and alert on any where `IsActive` is `false`.
* After reactivating, reconcile orders and stock against the API for the gap.

**How Hookdeck Can Help:** Hookdeck accepts each webhook as it arrives and stores it, so Cin7 Core never sees your downtime as failures. Hookdeck then delivers to your service with [retries](/docs/retries) on your schedule and lets you replay events after a fix.

### Two types have no EventType

**The Problem:** A handler that switches on `EventType` fails on stock level and overdue task webhooks.

**Why It Happens:** `Stock/AvailableStockLevelChanged` and `Task/Overdue` are documented with a JSON array body and no `EventType` field.

**Workarounds:**

* Give each webhook type its own URL path and route on the path rather than the body.
* Check whether the parsed body is an array before reading `EventType`.

**How Hookdeck Can Help:** Hookdeck keeps the path of every request, so [filters](/docs/filters) can route on it, and [transformations](/docs/transformations) can wrap array bodies in an object with an event type of your own.

### Basic auth is not covered

**The Problem:** A webhook set to `basicauth` never verifies in Hookdeck's Cin7 Core source.

**Why It Happens:** The source compares one named header with an exact value. It covers `bearerauth` and `ExternalHeaders`, not `basicauth`, and Cin7 Core's documentation does not say whether Basic credentials are sent with the first request.

**Workarounds:**

* Use `bearerauth`, or `noauth` plus a secret in `ExternalHeaders`.

**How Hookdeck Can Help:** With a bearer token or custom header, the source compares the value you enter. Hookdeck answers Cin7 Core with a `200` either way, but a request with a missing or wrong value is recorded as failed verification and never reaches your service.

### Inconsistent payloads

**The Problem:** Each type has its own shape, and some documented examples are not valid JSON.

**Why It Happens:** There is no shared envelope: each type's body is flat and specific to it. A few of Cin7 Core's own examples, such as `Sale/Voided`, contain trailing commas, so they cannot be used as fixtures as they are.

**Workarounds:**

* Build fixtures from real deliveries, not from the documentation.
* Read only the fields you need, and treat the IDs (`SaleID`, `TaskID`, the product `ID` and so on) as the key to fetch full records from the API.

**How Hookdeck Can Help:** Hookdeck stores every request with its full body, so you can build fixtures from real traffic and replay them against your handler.

## Best practices

### One URL per type

The URL path is the one routing key that works for every type, including the two array bodies. Give each webhook its own path.

### Fetch full records when you need them

Many payloads carry only IDs and a few fields. Use `SaleID`, the purchase's `TaskID` or the product `ID` to read the full record from the API before acting on it.

### Deduplicate

Retries resend the same event. Key on the type, the record ID and a timestamp or status from the body where one exists. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Verify the header, store the event, return `200`, and do the work afterwards, so six failures never pile up. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Cin7 Core sends each webhook type as an unsigned JSON request authenticated by the bearer token or custom header you set when creating the webhook. Use `bearerauth` or a custom header, compare the whole value in constant time over HTTPS, route on the URL path because two types carry no `EventType`, and respond quickly so six failures never deactivate the webhook.

[Hookdeck Event Gateway](https://hookdeck.com) checks your header, acknowledges Cin7 Core immediately, and keeps every event until your service has processed it, so an outage on your side does not leave a webhook switched off.

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