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:
{
"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
ExternalURLto 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
200as soon as the event is stored, and do the work afterwards. - List your webhooks with
GET /webhookson a schedule and alert on any whereIsActiveisfalse. - 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, ornoauthplus a secret inExternalHeaders.
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 productIDand 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.