Webhooks
Channel ID: webhooks
Get raw stock updates for the store via webhook calls.
Sends a webhook call each time a stock status or price changes, or a new product is added to the retailer’s point of sale.
Channel parameters:
- string
webhookUrl
(required): HTTPS endpoint to call with a POST request and a JSON payload on stock changes. - string
secret
(required): Signing secret. Should be an alphanumeric string of between 12 and 64 characters long. - boolean
enhancedProductData
: Set totrue
to enable product information to be sent alongside inventory information, if available. Depending on your contract, there might be additional charges for including product data. Defaults tofalse
.
Each webhook call has a body that looks like this:
{
"action": "inventory.update",
"data": {
"id": "local:en:GB:000009779712345005",
"barcode": "9779712345005",
"status": "in_stock",
"price": 44.99,
"currency": "GBP",
"condition": "new",
"store": {
"id": "64491013-b1ea-452c-b93a-85d28c45ebf9",
},
"product": { // enhancedProductData only
"contentLanguage": "en",
"title": "Men's Classic T-Shirt",
"brand": "Hanes",
"description": "Stay comfortable all day long in this classic Hanes t-shirt. Made from soft and durable cotton, this shirt features a ribbed collar and double-needle stitching for long-lasting wear. Available in a variety of colors and sizes.",
"images": [
"https://catalog-media.near.st/images/tshirt_front.jpg"
],
"category": "Clothing",
"attributes": {
"size": "Medium",
"color": "Navy Blue"
},
"groupId": "123456"
},
"createdAt": "2022-01-10T16:03:30Z"
}
}
Limitations
- Only a single webhook integration can be set up per store. Setting up a new webhook will override the previously created webhook.
- Webhooks will be retried up to 3 times if the client responds with a non-2xx status code.
Webhook payload reference
The webhook payload will be a JSON object that consists of two keys:
action
: indicates the type of eventdata
: contains the details of the inventory item
The following values are available for action
:
webhook.enabled
- webhook has been added (initial ping)inventory.update
- inventory item updateinventory_source.update
- inventory integration status updatechannel.update
- channel integration status update
Webhook enabled: webhook.enabled
Example payload:
{
"action": "webhook.enabled",
"data": {
"retailer": {
"id": "f35135ff-873a-4c53-9246-40b1267884f5"
},
"store": {
"id": "c23ff0dc-3cdc-4215-a209-7fbf00415ace"
}
}
}
Inventory item update: inventory.update
Example payload:
{
"action": "inventory.update",
"data": {
"id": "local:en:GB:000009779712345005",
"barcode": "9779712345005",
"status": "in_stock",
"price": 44.99,
"currency": "GBP",
"condition": "new",
"store": {
"id": "64491013-b1ea-452c-b93a-85d28c45ebf9",
},
"product": { // enhancedProductData only
"contentLanguage": "en",
"title": "Men's Classic T-Shirt",
"brand": "Hanes",
"description": "Stay comfortable all day long in this classic Hanes t-shirt. Made from soft and durable cotton, this shirt features a ribbed collar and double-needle stitching for long-lasting wear. Available in a variety of colors and sizes.",
"images": [
"https://catalog-media.near.st/images/tshirt_front.jpg"
],
"category": "Clothing",
"attributes": {
"size": "Medium",
"color": "Navy Blue"
},
"groupId": "123456"
},
"createdAt": "2022-01-10T16:03:30Z"
}
}
Payload keys:
Key | Description |
---|---|
id | String. A normalized ID that represents the unique inventory item. |
barcode | String. Plain barcode as represented in the retailer's point-of-sale system. For custom products, this can contain a non-numeric product ID. |
status | Enum string. Indicates the stock availaibility of the product: |
price | Number. 2 decimal current product price. Includes VAT where applicable. |
currency | String. ISO 4217 (opens in a new tab) 3-letter currency code (e.g. GBP, EUR, USD) for the price. |
condition | Enum string. Indicates the product condition, which can be: |
createdAt | String. ISO datetime representing when the inventory item update was created. |
store.id | Represents the store ID to which the inventory item belongs. |
product.title | Optional string, might be null . Product title, if supplied by the retailer or found in NearSt's global product catalog. |
product.contentLanguage | Optional string, might be null . Language of the content, represented as a lowercased two-letter ISO 639-1 (opens in a new tab) language code. Currently always returns en . In the future we will enable clients to specify a preferred content language. |
product.brand | Optional string, might be null . Brand of the product. In the case of media like books, this might contain the author's name. |
product.description | Optional string, might be null . Long-form description of the product. Might contain basic HTML. |
product.images | Optional array of strings, might be null . One or more images for the product. For most products, only a single image will be present. |
product.category | Optional string, might be null . It specifies the product’s main category. |
product.attributes | Optional object, might be null . Keys color and size are the only currently supported properties, but this might be extended in the future. |
product.groupId | Optional string, might be null It denotes a unique identifier for the group of products. |
Inventory source status update: inventory_source.update
Example payload:
{
"action": "inventory_source.update",
"data": {
"store": {
"id": "1bd8cc75-c78c-48ce-a037-b11255b08031"
},
"retailer": {
"id": "7c3076cc-720a-4c9b-9ece-d2fc97e5db1d"
},
"status": "connected", // or disconnected, pending, invalid
"provider": { // might be null if pending
"id": "eposnow",
"label": "EposNow Back-office",
"type": "point-of-sale",
"vendor": "EposNow"
},
"latestIngest": { // might be null if pending
"createdAt": "2022-01-10T11:46:03Z",
"numberOfLines": 2035,
"validLines": 1867,
"inStockValidLines": 394
},
"warnings": []
}
Channel status update: channel.update
Example payload:
{
"action": "channel.update",
"data": {
"store": {
"id": "1bd8cc75-c78c-48ce-a037-b11255b08031"
},
"retailer": {
"id": "7c3076cc-720a-4c9b-9ece-d2fc97e5db1d"
},
"status": "pending",
"channel": {
"id": "google",
"label": "Google Local Listings",
"description": "..."
},
"warnings": []
}
Example - setting up a webhook
After creating a retailer and creating a store within the retailer account, you can enable the webhook channel by doing a POST request to the /channels endpoint:
POST https://partner-api.near.st/retailers/{retailerId}/stores/{storeId}/channels
Content-Type: application/json
{
"type": "webhook",
"parameters": {
"webhookUrl": "https://my-service.com/webhooks/nearst",
"enhancedProductData": true,
"secret": "319218c9d1af43cbb0fca550c9809929"
}
}
This will send a verification webhook call of the type webhook.enabled
:
POST https://my-service.com/webhooks/nearst
Content-Type: application/json
X-Webhook-Signature: 2ed7180ae9cae96a0dfc82655686c284faf30d95eaa6e858796199334c56c6d6
{
"action": "webhook.enabled",
"data": {
"retailer": { "id": "f35135ff-873a-4c53-9246-40b1267884f5" },
"store": { "id": "c23ff0dc-3cdc-4215-a209-7fbf00415ace" }
}
}
Example - validating webhook payload
Every webhook call include a HTTP header X-Webhook-Signature
, which contains a HMAC SHA256 digest of the payload signed with the webhook secret defined during the webhook setup.
This allows you to verify the webhook has been sent by NearSt, rather than by a malicious third party trying to change the data you display.
Verifying the signature is relatively simple, here is an example using Javascript:
const crypto = require('crypto');
// Store your signing secret somewhere safe!
const secret = process.env.WEBHOOK_SECRET;
// Get the signature from the HTTP headers
const signature = request.headers['X-Webhook-Signature'];
// Calculate the digest from the HTTP body
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(request.rawBody).digest('hex');
// Compare signature to digest
if (signature.length !== digest.length || !crypto.timingSafeEqual(digest, signature)) {
throw new Error(`Request body digest (${digest}) did not match X-Webhook-Signature: ${signature}`);
}
// If we pass - all is good!