Gareth Wilson Gareth Wilson

Guide to Cin7 Core Webhooks: Features and Best Practices

Published


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

FeatureDetails
RequirementThe Automation module
ConfigurationAPI: POST https://inventory.dearsystems.com/ExternalApi/v2/webhooks
Subscription modelOne Type per webhook, at most 5 webhooks per type
FormatJSON
Event fieldEventType in the body, except for two types with array bodies
Authenticationnoauth, basicauth or bearerauth, plus optional ExternalHeaders
Signature and timestampNone
Retries6 attempts: 1 minute after the event, then 5, 10, 15, 20 and 25 minutes after the previous attempt
After the last failureThe webhook is deactivated
Method, timeout, source IPsNot documented

Common events

Cin7 Core documents 32 webhook types:

AreaTypes
SalesSale/Created, Sale/QuoteAuthorised, Sale/OrderAuthorised, Sale/Backordered, Sale/PickAuthorised, Sale/PackAuthorised, Sale/ShipmentAuthorised, Sale/InvoiceAuthorised, Sale/CreditNoteAuthorised, Sale/Voided, Sale/Undo
Sale payments and detailsSale/PartialPaymentReceived, Sale/FullPaymentReceived, Sale/AttachmentAdded, Sale/AdditionalAttributesChanged, Sale/ShipmentTrackingNumberChanged
PurchasesPurchase/OrderAuthorised, Purchase/InvoiceAuthorised, Purchase/StockReceivedAuthorised, Purchase/CreditNoteAuthorised, Purchase/Updated
Contacts and productsCustomer/Updated, Supplier/Updated, Product/Updated
StockStock/AvailableStockLevelChanged
CRMLead/Updated, Lead/Converted, Opportunity/Authorized, Opportunity/AttachmentAdded, Opportunity/Voided, Opportunity/Converted
TasksTask/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:

{
  "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 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:

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.

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:

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 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 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 can route on it, and 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.

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.

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 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 for free and handle Cin7 Core webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.