...
Table of Contents |
---|
minLevel | 1 |
---|
maxLevel | 6 | include4 |
---|
outline | false | indent |
---|
style | default |
---|
exclude | What is a webhook?|Implementing a Webhook in OneStock |
---|
type | list | class |
---|
printable | true |
---|
|
1. Configure your site for webhooks
...
Expand |
---|
|
Code Block |
---|
| {
"retry_intervals": [
30,
3060,
30120,
30240,
30480,
840
30
],
"retries_until_failure": 3,
"on_failure": {
"contact_emails": [
"contact@onestock-retailemail@company.com"
],
"contact_mobiles": [
"+33600000000"
],
"sms_notification_name": "webhook_failure",
"email_notification_name": "webhook_failure"
},
"on_deactivation": {
""contact_emails": [
"contact@onestock-retail"email@company.com"
],
"contact_mobiles": [
"+33600000000"
],
"sms_notification_name": "webhook_failure_recovered",
"email_notification_name": "webhook_failure_recovereddeactivation"
},
"on_failure_recovered": {
"contact_emails": [
"contact@onestock-retailemail@company.com"
],
"contact_mobiles": [],
"sms_notification_name": "",
"+33600000000"
],
"sms_notification_name": "webhook_email_notification_name": "webhook_failure_recovered",
"email_notification_name": "webhook_failure_recovered"
}
} |
|
...
The default retry delay values are set as shown above in seconds. In this instance the webhook would make the first call and if no response or confirmation is received then it will retry 6 times, first after 30 seconds, then 60, then 2 minutes, 4 minutes, 8 minutes and finally 16 minutes. However, after the 3rd failed attempt, i.e. one with no response, it will send a notification of failure to the contact details in the configuration.
...
3. Subscribe your webhook to a topic
In order to subscribe To listen to OneStock events you will subcribe your webhook to a topic, add the topic to the code, as shown in the example below on lines 5 and 6.
...
. We distinguish to types of topics, standard and custom. Standard topics work out of the box, you just need to sucribe your webhook to one of them and you will start receving all events that go through the topic event stream. Custom topics need slightly more configuration and allow you to listen to specific events that you will trigger trough your workflow.
Standard topics
1. Subcribe a webhook to a standard topic
To listent to topics, you simply need to subscribe a webhook to one or multiple topics, through a
POST /webhooks
call.
POST /webhooks
Code Block |
---|
{
"site_id": "MySiteID",
"token":"token",
"webhook": {
"httphash_methodkey": "POSTpersonnal_hash_key",
"http_method": "POST",
"status": "enabled",
"topics": [
"order_state_changed",
"parcel_state_changed",
"stockline_item_importgroup_errorstate_occurredchanged"
],
"url": "https://myWebhookURL" //you can generate a }
} |
Information about the expected responses can be found in our documentation here.
Available standard topics
...
disposbel one at https://webhook.site/
}
} |
Following a list of all topics we currently support. We may add more at any time, so in developing and maintaining your code, you should not assume that only these topics exist.
Topic | Displayed Name | Description | Message structure |
---|
Example |
---|
buffer_import_completed | Buffer Import Completed | Notifies when an async buffer import using API or file is completed | buffer_import_completed
Code Block |
---|
import_id: string
status: string |
|
buffer_import_error_occurred | Buffer Import Error Occurred | Notifies when a synchronous buffer import using API POST method fails | buffer_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
|
async_buffer_import_error_occurred | Asynchronous Buffer Import Error Occurred | Notifies when an asynchronous buffer import fails | async_buffer_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
|
customer Expand |
---|
title | Full list of standard topics |
---|
|
Topic | Displayed Name | Description | Message structure |
---|
buffer_import_completed |
|
...
Buffer Import Completed | Notifies when an async |
|
...
buffer import using API or file is completed |
|
...
buffer_import_completed
Code Block |
---|
import_id: string
status: string |
| |
|
...
buffer_import_error_occurred |
|
...
Buffer Import Error Occurred | Notifies when a synchronous |
|
...
buffer import using API POST method fails |
|
...
buffer_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
| async_ |
|
...
buffer_import_error_occurred | Asynchronous |
|
...
Buffer Import Error Occurred | Notifies when |
|
...
...
buffer import fails | async_
|
|
...
buffer_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
|
|
...
customer_import_completed |
|
...
Customer Import Completed | Notifies when an async |
|
...
customer import using API or file is completed |
|
...
customer_import_completed
Code Block |
---|
import_id: string
status: string |
|
|
...
customer_import_error_occurred |
|
...
Customer Import Error Occurred | Notifies when a synchronous |
|
...
customer import using API POST method fails |
|
...
customer_import_error_occurred
|
|
...
...
errors: []
entity_id?: string
message: string
date: int64 |
| async_customer_import_error_occurred | Asynchronous |
|
...
Customer Import Error Occurred | Notifies when |
|
...
...
customer import fails | async_
|
|
...
customer_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
|
|
...
transfer_import_completed |
|
...
Transfer Import Completed | Notifies when an async |
|
...
transfer import using API or file is completed |
|
...
transfer_import_completed
Code Block |
---|
import_id: string
status: string |
|
|
...
transfer_import_error_occurred |
|
...
Transfer Import Error Occurred | Notifies when a synchronous |
|
...
transfer import using API POST method fails |
|
...
transfer_import_error_occurred
|
|
...
...
...
request_body: string
date: int64 |
| async_ |
|
...
customer_import_error_occurred | Asynchronous |
|
...
Transfer Import Error Occurred | Notifies when an asynchronous |
|
...
transfer import fails | async_
|
|
...
transfer_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
|
|
...
...
Line Item Group State Change
...
Notifies of a state change for line item groups
line_item_group_state_changed
...
import_completed | Stock Import Completed | Notifies when an async stock import using API or file is completed | stock_import_completed
Code Block |
---|
import_id: string
|
|
|
...
...
| stock_import_error_occurred | Stock Import Error Occurred | Notifies when a synchronous stock import using API POST method fails | stock_import_error_occurred
|
|
...
error_content: string
import_id: string
|
|
|
...
...
...
...
...
...
order_state_changed
...
Order State Change
...
Notifies of a state change for orders
...
order_state_changed
Code Block |
---|
order_id: string
date: int64
old_state: string
new_state: string |
...
parcel_state_changed
...
| async_stock_import_error_occurred | Asynchronous Stock Import Error Occurred | Notifies when an asynchronous stock import fails | async_stock_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
| line_item_group_state_changed | Line Item Group State Change | Notifies of a state change for |
|
...
line item groups | line_item_group_state_changed
Code Block |
---|
order_id: string
date: int64
old_state: string
new_state: string
|
|
|
...
order_item_id: string
index_ranges: indexRanges
quantity: int64 |
|
|
...
...
changed | Order State Change | Notifies of |
|
...
line_item_group_entity_updated
, order_entity_updated
, parcel_entity_updated
Code Block |
---|
site_id: string
entity_id: string
entity_type: string
date: int64
diff: map[string]any |
...
orchestration_rules_changed
...
Orchestration Rules Changed
...
Allows triggering of preparation order on time.
When there is an orchestration rule change with a warehouse custom claim action triggered, line item groups are retrieved and a preparation order is sent to the warehouse.
orchestration_rules_changed
...
a state change for orders | order_state_changed
Code Block |
---|
order_id: string
date: int64
old_state: string
new_state: string |
| parcel_state_changed | Parcel State Change | Notifies of a state change for parcels | parcel_state_changed
Code Block |
---|
order_id: string
|
|
|
...
...
...
...
...
...
...
candidates_added
...
Candidates Added
...
Notifies that an endpoint is candidate for an order.
...
| entity_updated | Entity Update | Notifies of OMS entities information changes (parcels, line item groups and orders) POP (endpoint_orders, containers, piece_groups) and RM (return_parcels, and return_line_item_groups) | line_item_group_entity_updated , order_entity_updated , parcel_entity_updated
Code Block |
---|
site_id: string
entity_id: string
entity_type: string
date: int64
diff: map[string]any |
| orchestration_rules_changed | Orchestration Rules Changed | Allows triggering of preparation order on time. When there is an orchestration rule change with a warehouse custom claim action triggered, line item groups are retrieved and a preparation order is sent to the warehouse. | orchestration_rules_changed
Code Block |
---|
site_id: string
order_id: string
|
|
|
...
...
...
...
candidates_removed
...
Candidates Removed
...
Notifies when an endpoint is no longer candidate for an order.
candidates_removed
...
...
...
...
...
...
rules_over
...
Rules Over (custom notification)
...
Notifies of a rules over transition for an order.
orchestration_rules_over
(new version more accurate than rules_over)...
action: string
rule_index: string
line_item_index_ranges: indexRange
|
|
|
...
| candidates_added | Candidates Added | Notifies that an endpoint is candidate for an order. | candidates_added
Code Block |
---|
site_id: string
order_id: string
|
|
|
...
...
...
...
| candidates_removed | Candidates Removed | Notifies when an endpoint is no longer candidate for an order. | candidates_removed
Code Block |
---|
site_id: string
order_id: string
|
|
|
...
...
...
item_import_completed
...
...
item_import_completed
...
Item Import Completed
...
Notifies of a completed item import
rules_over Status |
---|
colour | Red |
---|
title | Before 12.4 |
---|
|
orchestration_rules_over |
|
item_import_error_occurred
...
Item Import Error Occurred
...
Notifies when a synchronous item import using API POST method fails
...
item_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
...
async_item_import_error_occurred
...
Asynchronous Item Import Error Occurred
...
Notifies when an asynchronous item import fails
...
async_item_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
...
psp_error_occurred
...
Payment Error
...
Notifies of errors during payment treatment
...
psp_error_occurred
Code Block |
---|
psp: string
request_route: string
request_body: string
response_body: string |
...
container_state_changed
operator_state_changed
endpoint_order_state_changed
piece_group_state_changed
...
Order Preparation Entity Change
...
Notifies of order preparation entity state changes (creation, update, removal)
...
container_state_changed, operator_state_changed
, endpoint_order_state_changed
and piece_group_state_changed
Code Block |
---|
entity_id string
entity_type string
site_id string
date string
old_state string
new_state string |
...
return_parcel_created
...
Return Parcel Creation
...
Notifies of the creation of a return parcel
return_parcel_created
:
...
...
Rules Over (custom notification) | Notifies of a rules over transition for an order. | orchestration_rules_over (new version more accurate than rules_over) Code Block |
---|
site_id: string
order_id: string
ruleset_id: string
line_item_index_ranges: IndexRanges |
rules_over
Code Block |
---|
site_id: string
order_id: string
ruleset_id: string |
back_from_rules_over
Code Block |
---|
site_id: string
order_id: string
ruleset_id: string |
| item_import_completed | Item Import Completed | Notifies of a completed item import | item_import_completed
Code Block |
---|
import_id: string
status: string |
| item_import_error_occurred | Item Import Error Occurred | Notifies when a synchronous item import using API POST method fails | item_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
| async_item_import_error_occurred | Asynchronous Item Import Error Occurred | Notifies when an asynchronous item import fails | async_item_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
| psp_error_occurred | Payment Error | Notifies of errors during payment treatment | psp_error_occurred
Code Block |
---|
psp: string
request_route: string
request_body: string
response_body: string |
| container_state_changed operator_state_changed endpoint_order_state_changed piece_group_state_changed | Order Preparation Entity Change | Notifies of order preparation entity state changes (creation, update, removal) | Code Block |
---|
entity_id string
entity_type string
site_id string
date string
old_state string
new_state string |
| return_parcel_created | Return Parcel Creation | Notifies of the creation of a return parcel | return_parcel_created :
Code Block |
---|
| order_id: string
id: string
user_id: string
creation_date: int64
return_note_id: string
document_id: string
last_update: int64
returned_line_items: []
creation_date: string
id: string
last_update: string
reason: string
receiver_comment: string
return_condition: string
sender_comment: string
item_id: string
state: string
information: map[string]any
state: string
delivery:
origin:
address:
endpoint_id: string
destination:
address:
id: string
city: string
contact:
title: string
first_name: string
last_name: string
company_name: string
phone_number: string
mobile_number: string
email: string
coordinates:
lon: float64
lat: float64
information: map[string]string
lines: []string
regions: map[string]
name: string
code: string
zip_code: string
endpoint_id: string
carrier:
name: string
option: string
tracking_code: string
tracking_link: string
information: map[string]any |
return_parcel_created@v3 :
Code Block |
---|
| documents: map[string]string
order_id: string
id: string
user_id: string
creation_date: int64
last_update: int64
return_line_item_groups: []
id: string
order_item_id: string
creation_date: int64
last_update: int64
reason: string
receiver_comment: string
return_condition: string
sender_comment: string
quantity: int64
line_item_index_ranges: IndexRanges
item_id: string
state: string
information: map[string]any
state: string
delivery:
origin:
address:
endpoint_id: string
destination:
address:
id: string
city: string
contact:
title: string
first_name: string
last_name: string
company_name: string
phone_number: string
mobile_number: string
email: string
coordinates:
lon: float64
lat: float64
information: map[string]string
lines: []string
regions: map[string]
name: string
code: string
zip_code: string
endpoint_id: string
carrier:
name: string
option: string
tracking_code: string
tracking_link: string
information: map[string]any
shipment:
id: string
document_id: string
tracking_code: string
tracking_link: string
transporter: string
carrier_data: string |
| return_line_item_group_state_changed return_parcel_state_changed | | Notifies of a state change for a return return_line_item_group or return_parcel | return_line_item_group_state_changed and return_parcel_state_changed
Code Block |
---|
| entity_id: string
entity_type: string
site_id: string
date: int64
old_state: string
new_state: string |
| line_items_reservations_updated | Line Items Reservations Update | Notifies of a reservation update on a line item group (global reservation to endpoint reservation, future stock to on hand, endpoint reservation removal) | line_items_reservations_updated
Code Block |
---|
| added_reservations: []
id: string
order_id: string
sales_channel: string
global: string
line_item_index_ranges: index_ranges
quantity: int64
item_id: string
endpoint_id: string
eta_start: int64
eta_end: int64
stock_type: string
purchase_order_number: string
remaining_line_item_index_ranges: index_ranges
removed_reservations: []
id: string
order_id: string
sales_channel: string
global: string
line_item_index_ranges: index_ranges
quantity: int64
item_id: string
endpoint_id: string
eta_start: int64
eta_end: int64
stock_type: string
purchase_order_number: string
remaining_line_item_index_ranges: index_ranges |
| stock_coverage_import_completed | Stock Coverage Import Completed | Notifies of a completed stock coverage import | stock_coverage_import_completed
Code Block |
---|
import_id: string
status: string |
| stock_coverage_import_error_occurred | Stock Coverage Import Error Occurred | Notifies when a synchronous stock coverage import using API POST method fails | stock_coverage_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
| async_stock_coverage_import_error_occurred | Asynchronous Stock Coverage Import Error Occurred | Notifies when an asynchronous stock coverage import fails | async_stock_coverage_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
| stock_disposition_import_completed | Stock Disposition Import Completed | Notifies of a completed stock disposition import | stock_disposition_import_completed
Code Block |
---|
import_id: string
status: string |
| stock_disposition_import_error_occurred | Stock Disposition Import Error Occurred | Notifies when a synchronous stock disposition import using API POST method fails | stock_disposition_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
| async_stock_disposition_import_error_occurred | Asynchronous Stock Disposition Import Error Occurred | Notifies when an asynchronous stock disposition import fails | async_stock_disposition_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
| endpoint_import_completed | Endpoint Import Completed | Notifies of a completed stock import | endpoint_import_completed
Code Block |
---|
import_id: string
status: string |
| endpoint_import_error_occurred | Endpoint Import Error Occurred | Notifies when a synchronous endpoint import using API POST method fails | endpoint_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
| async_endpoint_import_error_occurred | Asynchronous Endpoint Import Error Occurred | Notifies when an asynchronous endpoint import fails | async_endpoint_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
| user_import_completed | User Import Completed | Notifies of a completed user import | user_import_completed
Code Block |
---|
import_id: string
status: string |
| user_import_error_occurred | User Import Error Occurred | Notifies when a synchronous user import using API POST method fails | user_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
| async_user_import_error_occurred | Asynchronous User Import Error Occurred | Notifies when an asynchronous user import fails | async_user_import_error_occurred
Code Block |
---|
import_id?: string
errors: []
entity_id?: string
message: string
date: int64 |
| carrier_error_occurred | Carrier Error | Relays of errors during carrier communication | carrier_error_occurred
Code Block |
---|
carrier: string
request_route: string
request_body: string
response_body: string |
| tracking_link_created | | Notifies of the creation of a tracking link | tracking_link_created
Code Block |
---|
carrier: string
tracking_link: string |
| shipment_created | | Notifies of the creation of a shipment | shipment_created
Code Block |
---|
| carrier: string
endpoint_id: string
order_id: string
parcel_id: string
shipment_id: string
user_id: string |
| stock_export_completed | Stock Export Completed | Notifies the completion of a stock export by file (SFTP) | Code Block |
---|
export_id string
file_job_id string
exported_lines_count int64 |
|
|
...
import_error - custom topic
Notifies an error when importing entities async. All entities will be sent in through the same topic.
...
import_error
Code Block |
---|
|
object_id: string
object_type: string
params: {}
content: string
date: int
error: string |
...
2. Test that all works correctly
To do so, you can simply create a disposabel webhook endpoint going to https://webhook.site/ and use it as the url of your topic (by clicking on edit you can change the response code do 202
, only 100 webhook receptions are allowed for free). In the case of our example, when a parcel transitions from new
→ bagged
, as when bagged in the Store App, we should receive a body as the following
Code Block |
---|
{
"order_id": "DV00000007_MC",
"date": 1727862652,
"old_state": "new",
" |
...
...
...
...
parcel_id": "66fd147ab4fefe10957e4a1d"
} |
...
Custom topics
If the event you wish to subscribe to is not listed above you can create a custom topic. The custom topic can either notify based on an existing OneStock eventevents, or based on an event triggered in the workflow.
A custom topic Custom topics can be created either via API, or via the Back Office Configuration screens.
1.
...
Create a custom notification
Go to Configuration > Outbound Messages > Notifications. A default configuration will be shown in JSON, which can be modified or added to in order to create a new topic notification.
...
In order to configure a webhook via API the topic must be set using the route POST /webhook_topics
as shown below.
...
...
, and add a custom notification. Base yourself on the following code example and tailor it to your need by modifing the id and the topic id. Leave the rest unchanged.
Code Block |
---|
"email_sent_to_client":{ //notification id, we will reference it in the workflow
"media":{
"webhook_topic":[
{
"topic": "myCustomTopic",
"ordered": false
"email_sent_to_client_topic" //topic id, we will then create it through API
]
},
} |
Triggering custom notification from the workflow
Add a send notification event in the transition that must trigger the custom notification, an set the custom name, in our case, import_error
, but this could be any name you configur in your notifications.
...
4. Acknowledgment of Webhook Receipt
Upon receiving a message via webhook, it is essential to return a 202 Accepted
response promptly (within 15 seconds) after verifying the message’s legitimacy (optional but recommended). If this acknowledgment is not received in time, the system will retry sending the message according to the retry_intervals
configuration, if retries are not acknowledged promptly
Legitimacy Check
While optional, OneStock strongly recommends performing a legitimacy check to ensure that the webhook message is genuinely sent by the OneStock system.
Each webhook message includes a signature in the header with the following format: timestamp + "." + signature
.
During webhook setup (POST /webhooks
), a hash key is either provided by the client or generated by OneStock. This key, referred to as h0
, is used to encrypt the signature. OneStock retains the three most recent hash keys (h0, h1, h2) to support ongoing signature verification.
The signature format is:
t=timestamp.h0=h[0],h1=h[1],h2=h[2]
, where:
timestamp
: The current timestamp.
h[0]
: The signature encrypted using the latest hash key.
h[1]
: The signature encrypted using the previous hash key.
h[2]
: The signature encrypted using the oldest hash key.
Examples of signature and verification code
Expand |
---|
title | Example of signature model |
---|
|
Current timestamp = 1704092400 Encryption of h0 for 1704092400 = 1234 (encryption of the timestamp with the new key) Encryption of h1 for 1704092400 = 9876 (encryption of the timestamp with the previous key) The signature will be the following : t=1704092400.h0=1234.h1=9876 Resulting the header : Onestock-Signature=t=1704092400.h0=1234,h1=9876 |
Expand |
---|
title | Example of signature verification in Php |
---|
|
Code Block |
---|
|
|
<?php
// checkWebhookSignatureWithMultipleKeys check webhook signature with multiple secret keys.
function checkWebhookSignatureWithMultipleKeys($request, $keys) {
// Iterates through each provided secret key.
foreach ($keys as $key) {
// Calls the checkWebhookSignature function with the current request and secret key.
$isValid = checkWebhookSignature($request, $key);
// If the signature is valid with the current key, return true immediately.
if ($isValid) {
return true;
}
}
// If none of the keys validate the signature, return false.
return false;
}
// checkWebhookSignature check the validity of the webhook signature.
function checkWebhookSignature($request, $secretKey) {
// Extract the 'Onestock-Signature' header from the incoming request.
// This header contains the signature to be verified, formatted as "t=timestamp.h0=hash0,h1=hash1..."
$signatureHeader = $request->headers->get('Onestock-Signature');
// Read the raw body of the request. This is necessary to reconstruct the payload for hashing and verification.
$body = file_get_contents('php://input');
// Split the signature header into individual components based on the '.' delimiter.
// This separates the timestamp and each hash value for individual processing.
$signatureParts = explode('.', $signatureHeader);
// Ensure that the signature contains at least a timestamp and one hash value.
// If not, the signature is considered incomplete and thus invalid.
if (count($signatureParts) < 2) {
return false;
}
// Extract the timestamp from the first part of the signature, removing the 't=' prefix.
// The timestamp is used to verify the timeliness of the request to prevent replay attacks.
$timestamp = substr($signatureParts[0], 2);
// Check if the current time minus the provided timestamp exceeds 6 hours (21600 seconds).
// If so, consider the request too old and reject it to prevent replay attacks.
if (time() - intval($timestamp) > 60 * 60 * 6) {
return false;
}
// Concatenate the timestamp and the request body with a '.' to reconstruct the original payload.
// This payload mirrors what was used to generate the hash on the sender's side.
$payload = $timestamp . '.' . $body "time_type": "duration",
"time_value": "0s"
}
...
2. Create the custom topic
In order to configure a webhook via API the topic must be set using the route POST /webhook_topics
as shown below.
POST /webhook_topics
Code Block |
---|
|
{
"site_id": "My_Site_ID",
"token":"token",
"webhook_topic": {
"topic": "email_sent_to_client_topic", //custom topic id
"ordered": true //messages order will be respected
}
} |
3. Subcribe a webhook to the custom topic
We must add a listener to the custom topic. To do so, we just need to create a webhook using the route POST /webhooks
POST /webhooks
Code Block |
---|
{
"site_id": "MySiteID",
"token":"token",
"webhook": {
"hash_key": "personnal_hash_key",
"http_method": "POST",
"status": "enabled",
"topics": [
"email_sent_to_client_topic"
],
"url": "https://myWebhookURL" //you can generate a disposbel one at https://webhook.site/
}
} |
4. Trigger the custom notification from the workflow
Add a send notification event in the transition that must trigger the custom notification, an set the custom name, in our case, email_sent_to_client
...
Video walkthrough
...
5. Test that all works correctly
Note |
---|
Upon receiving a message via webhook, it is essential to return a 202 Accepted response promptly (within 15 seconds, but ideally under 300 ms) after verifying the message’s legitimacy (optional but recommended) |
To do so, you can simply create a disposabel webhook endpoint going to https://webhook.site/ and use it as the url of your topic (by clicking on edit you can change the response code do 202
, only 100 webhook receptions are allowed for free). In the case of our example, when a parcel transitions from new
→ bagged
, as when bagged in the Store App, we should receive a body as the following
Code Block |
---|
|
{
"object_id": "66fd0deab4fefe10957e49fe",
"object_type": "parcel",
"order_id": "DV00000007_MC",
"params": {
"information": "triggered from workflow"
}
} |
...
4. Acknowledgment of Webhook Receipt
Note |
---|
Upon receiving a message via webhook, it is essential to return a 202 Accepted response promptly (within 15 seconds, but ideally under 300 ms) after verifying the message’s legitimacy (optional but recommended) |
Upon receiving a message via webhook, it is essential to return a 202 Accepted
response promptly (within 15 seconds, but ideally under 300 ms) after verifying the message’s legitimacy (optional but recommended). If this acknowledgment is not received in time, the system will retry sending the message according to the retry_intervals
configuration, if retries are not acknowledged under 15 seconds.
To maintain synchronization between OneStock and your service, please acknowledge incoming webhook messages immediately. Delays can cause desynchronization and data inconsistencies. We recommend processing messages asynchronously and sending acknowledgments promptly to ensure smooth integration.
Legitimacy Check
While optional, OneStock strongly recommends performing a legitimacy check to ensure that the webhook message is genuinely sent by the OneStock system.
Each webhook message includes a signature in the header with the following format: timestamp + "." + signature
.
During webhook setup (POST /webhooks
), a hash key is either provided by the client or generated by OneStock. This key, referred to as h0
, is used to encrypt the signature. OneStock retains the three most recent hash keys (h0, h1, h2) to support ongoing signature verification.
The signature format is:
t=timestamp.h0=h[0],h1=h[1],h2=h[2]
, where:
timestamp
: The current timestamp.
h[0]
: The signature encrypted using the latest hash key.
h[1]
: The signature encrypted using the previous hash key.
h[2]
: The signature encrypted using the oldest hash key.
Examples of signature and verification code
Expand |
---|
title | Example of signature model |
---|
|
Current timestamp = 1704092400 Encryption of h0 for 1704092400 = 1234 (encryption of the timestamp with the new key) Encryption of h1 for 1704092400 = 9876 (encryption of the timestamp with the previous key) The signature will be the following : t=1704092400.h0=1234.h1=9876 Resulting the header : Onestock-Signature=t=1704092400.h0=1234,h1=9876 |
Expand |
---|
title | Example of signature verification in Php |
---|
|
Code Block |
---|
| <?php
// checkWebhookSignatureWithMultipleKeys check webhook signature with multiple secret keys.
function checkWebhookSignatureWithMultipleKeys($request, $keys) {
// Iterates through each provided secret key.
foreach ($keys as $key) {
// Calls the checkWebhookSignature function with the current request and secret key.
$isValid = checkWebhookSignature($request, $key);
// If the signature is valid with the current key, return true immediately.
if ($isValid) {
return true;
}
}
// If none of the keys validate the signature, return false.
return false;
}
// checkWebhookSignature check the validity of the webhook signature.
function checkWebhookSignature($request, $secretKey) {
// Extract the 'Onestock-Signature' header from the incoming request.
// This header contains the signature to be verified, formatted as "t=timestamp.h0=hash0,h1=hash1..."
$signatureHeader = $request->headers->get('Onestock-Signature');
// ComputeRead the raw body expectedof signaturethe byrequest. hashingThis theis reconstructednecessary payloadto withreconstruct the providedpayload secretfor keyhashing usingand SHA-256verification.
$expectedSignature$body = hashfile_get_hmaccontents('sha256', $payload, $secretKey);php://input');
// IterateSplit overthe eachsignature hashheader valueinto inindividual the signature (excludingcomponents based on the '.' delimiter.
// This separates the timestamp).
and each hash value for ($i = 1; $i < count($signatureParts); $i++) {individual processing.
$signatureParts = explode('.', $signatureHeader);
// Ensure Extractthat the hashsignature value,contains removingat theleast leading "hX=" where X is the hash indexa timestamp and one hash value.
// ThisIf isnot, donethe bysignature cuttingis offconsidered theincomplete prefixand based on its lengththus invalid.
$h = substr($signatureParts[$i], strlen("h{$i}="));
if (count($signatureParts) < 2) {
return false;
}
// CompareExtract the timestamp extractedfrom the hashfirst valuepart withof the expected signature.
signature, removing the 't=' prefix.
// The Iftimestamp anyis matchused isto found,verify the signaturetimeliness isof consideredthe valid,request andto theprevent functionreplay returns trueattacks.
$timestamp = if ($h === $expectedSignature) {substr($signatureParts[0], 2);
// Check returnif true;the current time minus the }provided timestamp exceeds 6 hours }(21600 seconds).
// If no hash values match the expected signature, the signature is deemed invalid.
return false;
}
// Example usage of the function with a webhook request and a list of previous keys.
$request = /* The webhook request to be verified */;
$previousKeys = ['key1', 'key2', 'key3']; // List of previous keys to check against.
// Calls the new function to check the signature against the list of previous keys.
$isSignatureValid = checkWebhookSignatureWithMultipleKeys($request, $previousKeys);
if ($isSignatureValid) {
echo "The webhook signature is valid.";
} else {
echo "The webhook signature is invalid.";
}
?>
|
|
Expand |
---|
title | Example of signature verification in Python |
---|
|
Code Block |
---|
| import hmac
import hashlib
import time
# Function to check the signature of a webhook with multiple secret keys.
def check_webhook_signature_with_previous_keys(request, previous_keys):
"""
Attempts to verify the webhook signature against a list of previous secret keys.
:param request: The incoming webhook request object.
:param previous_keys: A list of previous secret keys to attempt verification with.
:return: True if the signature is verified successfully with any of the keys, otherwise False.
"""
for key in previous_keys:
# Attempt to verify the signature with the current key.
if check_webhook_signature(request, key):
# If verification is successful, return True immediately.
return True
# If none of the keys result in a successful verification, return False.
return False
def check_webhook_signature(request, secret_key):
"""
Verifies the webhook signature against a given secret key.
:param request: The incoming webhook request object, containing headers and data.
:param secret_key: The secret key used for verifying the signature.
:return: True if the signature is valid, otherwise False.
"""
signature_header = request.headers.get('Onestock-Signature')
body = request.data
signature_parts = signature_header.split('.')
if len(signature_parts) < 2:
return False
timestamp = signature_parts[0].removeprefix("t=")
if time.time() - int(timestamp) > 60 * 60 * 6:
return False
payload = f'{timestamp}.{body}'
expected_signature = compute_hash(secret_key, payload)
for i in range(1, len(signature_parts)so, consider the request too old and reject it to prevent replay attacks.
if (time() - intval($timestamp) > 60 * 60 * 6) {
return false;
}
// Concatenate the timestamp and the request body with a '.' to reconstruct the original payload.
// This payload mirrors what was used to generate the hash on the sender's side.
$payload = $timestamp . '.' . $body;
// Compute the expected signature by hashing the reconstructed payload with the provided secret key using SHA-256.
$expectedSignature = hash_hmac('sha256', $payload, $secretKey);
// Iterate over each hash value in the signature (excluding the timestamp).
for ($i = 1; $i < count($signatureParts); $i++) {
// Extract the hash value, removing the leading "hX=" where X is the hash index.
// This is done by cutting off the prefix based on its length.
$h = substr($signatureParts[$i], strlen("h{$i}="));
// Compare the extracted hash value with the expected signature.
// If any match is found, the signature is considered valid, and the function returns true.
if ($h === $expectedSignature) {
return true;
}
}
// If no hash values match the expected signature, the signature is deemed invalid.
return false;
}
// Example usage of the function with a webhook request and a list of previous keys.
$request = /* The webhook request to be verified */;
$previousKeys = ['key1', 'key2', 'key3']; // List of previous keys to check against.
// Calls the new function to check the signature against the list of previous keys.
$isSignatureValid = checkWebhookSignatureWithMultipleKeys($request, $previousKeys);
if ($isSignatureValid) {
echo "The webhook signature is valid.";
} else {
echo "The webhook signature is invalid.";
}
?>
|
|
Expand |
---|
title | Example of signature verification in Python |
---|
|
Code Block |
---|
| import hmac
import hashlib
import time
# Function to check the signature of a webhook with multiple secret keys.
def check_webhook_signature_with_previous_keys(request, previous_keys):
"""
Attempts to verify the webhook signature against a list of previous secret keys.
:param request: The incoming webhook request object.
:param previous_keys: A list of previous secret keys to attempt verification with.
:return: True if the signature is verified successfully with any of the keys, otherwise False.
"""
for key in previous_keys:
# Attempt to verify the signature with the current key.
if check_webhook_signature(request, key):
h = signature_parts[i].removeprefix(f'h{i}=') # If verification is successful, return True if hmac.compare_digest(h, expected_signature):immediately.
return True
# If none of the keys result in a successful verification, return False.
return False
def computecheck_webhook_hashsignature(request, secret_key, payload):
"""
ComputesVerifies the HMACwebhook SHA-256 hash ofsignature against a payload usinggiven a secret key.
:param secret_keyrequest: The secret key as a stringincoming webhook request object, containing headers and data.
:param payloadsecret_key: The payloadsecret tokey hash,used typicallyfor consisting ofverifying the timestamp and request bodysignature.
:return: TheTrue if computedthe hashsignature asis avalid, hexadecimalotherwise stringFalse.
"""
return hmac.new(bytes(secret_key, 'utf-8'), msg=bytes(payload, 'utf-8'), digestmod=hashlib.sha256).hexdigest()
# Example usage
request = ... # The webhook request object
previous_keys = ['old_key1', 'old_key2', 'current_key']
verified = check_webhook_signature_with_previous_keys(request, previous_keys)
if verified:
print("Webhook signature verified successfully.")
else:
print("Failed to verify webhook signature.") |
|
Expand |
---|
title | Example of signature verification in Node.js |
---|
|
Code Block |
---|
| const crypto = require('crypto');
/**
* Attempts to verify the webhook signature against a list of previous secret keys.
*
* @param {object} req - The request object from the webhook, containing headers and body.
* @param {array} previousKeys - An array of secret keys (including the current and any previous ones) to try for verification.
* @returns {boolean} - Returns true if the signature is valid with any of the provided keys, otherwise false.
*/
function verifyWithPreviousKeys(req, previousKeys) {
for (const key of previousKeys) {
// Attempt to verify the signature with the currentsignature_header = request.headers.get('Onestock-Signature')
body = request.data
signature_parts = signature_header.split('.')
if len(signature_parts) < 2:
return False
timestamp = signature_parts[0].removeprefix("t=")
if time.time() - int(timestamp) > 60 * 60 * 6:
return False
payload = f'{timestamp}.{body}'
expected_signature = compute_hash(secret_key, payload)
for i in range(1, len(signature_parts)):
h = signature_parts[i].removeprefix(f'h{i}=')
if hmac.compare_digest(h, expected_signature):
return True
return False
def compute_hash(secret_key, payload):
"""
Computes the HMAC SHA-256 hash of a payload using a secret key.
if
(checkWebhookSignature(req, key)) { :param secret_key: The // If verification succeeds with the current key, return true immediatelysecret key as a string.
:param payload: returnThe true;payload to hash, typically consisting }of the timestamp }and request body.
// If verification fails with all keys, return false.
return false;
}
/**
* Checks the validity of the webhook signature to ensure the request is from a trusted source.
*
* @param {object} req - The request object from the webhook, containing headers and body.
* @param {string} secretKey - The secret key used to generate the signature for comparison.
* @returns {boolean} - Returns true if the signature is valid, otherwise false.
*/
function checkWebhookSignature(req, secretKey) {
const signatureHeader = req.headers['Onestock-Signature'];
const body = req.body;
const signatureParts = signatureHeader.split(',');
if (signatureParts.length < 2) {
return false;
}
const timestamp = signatureParts[0].replace('t=', '');
if (Date.now() / 1000 - parseInt(timestamp) > 60 * 60 * 6 :return: The computed hash as a hexadecimal string.
"""
return hmac.new(bytes(secret_key, 'utf-8'), msg=bytes(payload, 'utf-8'), digestmod=hashlib.sha256).hexdigest()
# Example usage
request = ... # The webhook request object
previous_keys = ['old_key1', 'old_key2', 'current_key']
verified = check_webhook_signature_with_previous_keys(request, previous_keys)
if verified:
print("Webhook signature verified successfully.")
else:
print("Failed to verify webhook signature.") |
|
Expand |
---|
title | Example of signature verification in Node.js |
---|
|
Code Block |
---|
| const crypto = require('crypto');
/**
* Attempts to verify the webhook signature against a list of previous secret keys.
*
* @param {object} req - The request object from the webhook, containing headers and body.
* @param {array} previousKeys - An array of secret keys (including the current and any previous ones) to try for verification.
* @returns {boolean} - Returns true if the signature is valid with any of the provided keys, otherwise false.
*/
function verifyWithPreviousKeys(req, previousKeys) {
for (const key of previousKeys) {
return// false;Attempt to verify }the signature with the const payload = `${timestamp}.${body}`;current key.
const expectedSignature = crypto.createHmac('sha256', secretKey).update(payload).digest('hex');
console.log(expectedSignature)if (checkWebhookSignature(req, key)) {
for (let// iIf =verification 1;succeeds iwith < signatureParts.length; i++) {
the current key, return true immediately.
const h = signatureParts[i].replace(`h${i-1}=`, '') return true;
if (h === expectedSignature) {
return true;
}
}
// If verification fails with all keys, return false.
return false;
}
/**
} * Checks the returnvalidity false;of }the webhook //signature Exampleto usageensure constthe reqrequest =is {from a trusted headers:source.
{ *
'Onestock-Signature': 't=timestamp.h0=hash0.h1=eb2a2ab3d29587fba099451925bdbd9637814711c97bc79c6a6e86f898884796'
},
body: 'request body'
};
const previousKeys = ['oldKey1', 'oldKey2', 'currentKey'];
const isVerified = verifyWithPreviousKeys(req, previousKeys);
console.log(`Webhook signature verification result: ${isVerified}`); |
|
Expand |
---|
title | Example of signature verification in Ruby |
---|
|
Code Block |
---|
| require 'openssl'
require 'time'
# Method to attempt verification of a webhook signature with multiple secret keys.
# @param request [Object] The incoming request object, containing headers and a body.
# @param previous_keys [Array<String>] An array of secret keys to attempt for signature verification.
# @return [Boolean] True if the signature is verified successfully with any of the keys, otherwise false.
def verify_signature_with_previous_keys(request, previous_keys)
# Iterates through each provided secret key to attempt signature verification.
previous_keys.each do |key|
# If the signature verification is successful with the current key, return true immediately.
if check_webhook_signature(request, key) {
return true
}
end
# If none of the keys result in a successful verification, return false.
return false
end
# Defines a method to check the validity of the webhook signature against the provided secret key.
# @param request [Object] The incoming request object, which contains headers and a body.
# @param secret_key [String] The secret key used for generating and comparing the signature.
# @return [Boolean] True if the signature is valid, otherwise false.
def check_webhook_signature(request, secret_key)
# Extracts the signature from the request's headers. Expected format: “t=timestamp.h0=h[0].h1=h[1].h2=h[2].h3=h[3]"
signature_header = request.headers['HTTP_ONESTOCK_SIGNATURE']
# Reads the request body as a string.
body = request.body.read
# Splits the signature header into its constituent parts: the timestamp and the hash values.
signature_parts = signature_header.split(',')
# Ensures that there are at least two parts in the signature (the timestamp and at least one hash).
if signature_parts.size < 2
return false
end
# Extracts and converts the timestamp from the first part of the signature, removing 't=' prefix.
timestamp = signature_parts[0].sub('t=', '').to_i
# Verifies that the request is not too old (more than 6 hours in this case) to avoid replay attacks.
if Time.now.to_i - timestamp > 60 * 60 * 6
return false
end
# Constructs the payload by concatenating the timestamp and the body, separated by a period.
# This payload is used as the basis for signature generation and comparison.
payload = "#{timestamp.to_s}.#{body}" # Format: "timestamp.body"
# Generates the expected signature by hashing the payload with the secret key using SHA-256.
expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload)
# Iterates over each hash part in the signature, skipping the first part (the timestamp).
signature_parts.drop(1).each_with_index do |part, index|
# Removes the hash prefix ('hX=') to isolate the hash value for comparison.
h = part.sub("h#{index}=", '')
# If any of the hash values matches the expected signature, the signature is deemed valid.
if h == expected_signature
return true
end
end
# Example usage of the new method:
request = # Assume this is the incoming webhook request object.
previous_keys = ['old_secret_key1', 'old_secret_key2', 'current_secret_key']
if verify_signature_with_previous_keys(request, previous_keys)
puts "Webhook signature verified successfully."
else
puts "Failed to verify webhook signature."
end
|
|
Webhook Acknowledgment Best Practices
To avoid blocking the entire message queue when processing webhooks, it is crucial to focus on the signature validation first and handle any additional checks asynchronously.
Recommended Acknowledgment Process:
Signature Validation:
Objective: Ensure the message is legitimately sent by OneStock.
Action: If the signature is valid, immediately return an HTTP 202 Accepted
response.
Asynchronous Content Validation (if applicable):
Key Benefits
Prevent Queue Blocking: By acknowledging based solely on signature validation, the message queue remains smooth and uninterrupted.
Efficient Processing: Additional content checks can be handled asynchronously, avoiding potential bottlenecks.
5. Retries and Error Management
If acknowledgment is not received within the configured time frame, the system will retry sending the message. Notifications regarding errors can be sent via SMS, email, or both.
Reponse Statuses:
on_failure
: Triggered after the maximum number of retries (retries_until_failure
) is reached without acknowledgment. This indicates that the message failed to be delivered.
on_failure_recovered
: Triggered when a failed message is later acknowledged during a retry. The retry count resets to 0.
on_deactivation
: Triggered when the maximum retry attempts are exhausted without recovery. The webhook is automatically disabled and must be manually reactivated.
To resolve these errors, ensure your web service is functional and restart the webhook by setting it’s status to enabled
with a PATCH /webhooks/:id/status.
Code Block |
---|
PATCH /webhooks/:id/status
{ "site_id": "{{site_id}}", "token": "{{token}}", "status": "enabled" } |
6. Webhook Statuses
enabled
: The default state after creation. Messages are processed normally, triggering the webhook.
Paused:
Messages are stored while the webhook is in a paused state. The behavior of this state depends on the scenario:
Manual pause: No messages will be sent until the webhook is manually re-enabled.
Automatic pause: This occurs when a message is not acknowledged by the receiver within 15 seconds. In this case, the message will be retried. If the message fails to be acknowledged after the configured number of retries (retries_until_failure
), an on_failure notification will be triggered (via email and/or SMS, depending on configuration). If the maximum number of retries is reached without success, the webhook will be disabled
. However, if the message is acknowledged before reaching the retry limit, the webhook will automatically resume sending messages (enabled
).
disabled
: The webhook stops functioning and storing messages after an on_deactivation
error. It must be manually reactivated.
7. Message storage
Messages are stored for a week; those older than a week are automatically deleted.
8. Integration best practices
...
* @param {object} req - The request object from the webhook, containing headers and body.
* @param {string} secretKey - The secret key used to generate the signature for comparison.
* @returns {boolean} - Returns true if the signature is valid, otherwise false.
*/
function checkWebhookSignature(req, secretKey) {
const signatureHeader = req.headers['Onestock-Signature'];
const body = req.body;
const signatureParts = signatureHeader.split(',');
if (signatureParts.length < 2) {
return false;
}
const timestamp = signatureParts[0].replace('t=', '');
if (Date.now() / 1000 - parseInt(timestamp) > 60 * 60 * 6) {
return false;
}
const payload = `${timestamp}.${body}`;
const expectedSignature = crypto.createHmac('sha256', secretKey).update(payload).digest('hex');
console.log(expectedSignature)
for (let i = 1; i < signatureParts.length; i++) {
const h = signatureParts[i].replace(`h${i-1}=`, '');
if (h === expectedSignature) {
return true;
}
}
return false;
}
// Example usage
const req = {
headers: {
'Onestock-Signature': 't=timestamp.h0=hash0.h1=eb2a2ab3d29587fba099451925bdbd9637814711c97bc79c6a6e86f898884796'
},
body: 'request body'
};
const previousKeys = ['oldKey1', 'oldKey2', 'currentKey'];
const isVerified = verifyWithPreviousKeys(req, previousKeys);
console.log(`Webhook signature verification result: ${isVerified}`); |
|
Expand |
---|
title | Example of signature verification in Ruby |
---|
|
Code Block |
---|
| require 'openssl'
require 'time'
# Method to attempt verification of a webhook signature with multiple secret keys.
# @param request [Object] The incoming request object, containing headers and a body.
# @param previous_keys [Array<String>] An array of secret keys to attempt for signature verification.
# @return [Boolean] True if the signature is verified successfully with any of the keys, otherwise false.
def verify_signature_with_previous_keys(request, previous_keys)
# Iterates through each provided secret key to attempt signature verification.
previous_keys.each do |key|
# If the signature verification is successful with the current key, return true immediately.
if check_webhook_signature(request, key) {
return true
}
end
# If none of the keys result in a successful verification, return false.
return false
end
# Defines a method to check the validity of the webhook signature against the provided secret key.
# @param request [Object] The incoming request object, which contains headers and a body.
# @param secret_key [String] The secret key used for generating and comparing the signature.
# @return [Boolean] True if the signature is valid, otherwise false.
def check_webhook_signature(request, secret_key)
# Extracts the signature from the request's headers. Expected format: “t=timestamp.h0=h[0].h1=h[1].h2=h[2].h3=h[3]"
signature_header = request.headers['HTTP_ONESTOCK_SIGNATURE']
# Reads the request body as a string.
body = request.body.read
# Splits the signature header into its constituent parts: the timestamp and the hash values.
signature_parts = signature_header.split(',')
# Ensures that there are at least two parts in the signature (the timestamp and at least one hash).
if signature_parts.size < 2
return false
end
# Extracts and converts the timestamp from the first part of the signature, removing 't=' prefix.
timestamp = signature_parts[0].sub('t=', '').to_i
# Verifies that the request is not too old (more than 6 hours in this case) to avoid replay attacks.
if Time.now.to_i - timestamp > 60 * 60 * 6
return false
end
# Constructs the payload by concatenating the timestamp and the body, separated by a period.
# This payload is used as the basis for signature generation and comparison.
payload = "#{timestamp.to_s}.#{body}" # Format: "timestamp.body"
# Generates the expected signature by hashing the payload with the secret key using SHA-256.
expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload)
# Iterates over each hash part in the signature, skipping the first part (the timestamp).
signature_parts.drop(1).each_with_index do |part, index|
# Removes the hash prefix ('hX=') to isolate the hash value for comparison.
h = part.sub("h#{index}=", '')
# If any of the hash values matches the expected signature, the signature is deemed valid.
if h == expected_signature
return true
end
end
# Example usage of the new method:
request = # Assume this is the incoming webhook request object.
previous_keys = ['old_secret_key1', 'old_secret_key2', 'current_secret_key']
if verify_signature_with_previous_keys(request, previous_keys)
puts "Webhook signature verified successfully."
else
puts "Failed to verify webhook signature."
end
|
|
Webhook Acknowledgment Best Practices
To avoid blocking the entire message queue when processing webhooks, it is crucial to focus on the signature validation first and handle any additional checks asynchronously.
Recommended Acknowledgment Process:
Signature Validation:
Objective: Ensure the message is legitimately sent by OneStock.
Action: If the signature is valid, immediately return an HTTP 202 Accepted
response.
Asynchronous Content Validation (if applicable):
Key Benefits
Prevent Queue Blocking: By acknowledging based solely on signature validation, the message queue remains smooth and uninterrupted.
Efficient Processing: Additional content checks can be handled asynchronously, avoiding potential bottlenecks.
5. Retries, Error Management and Alerts
If acknowledgment is not received within the configured time frame, the system will retry sending the message. Notifications regarding errors can be sent via SMS, email, or both.
Events
on_failure
: Triggered after the maximum number of retries (retries_until_failure
) is reached without acknowledgment. Retries count do not include the original message (the first one which was not ackowledged).
on_failure_recovered
: Triggered when a failed message is later acknowledged during a retry. The retry count resets to 0.
on_deactivation
: Triggered when the maximum retry attempts are exhausted without recovery. The webhook is automatically disabled and must be manually reactivated.
To resolve these errors, ensure your web service is functional and restart the webhook by setting it’s status to enabled
with a PATCH /webhooks/:id/status.
Code Block |
---|
PATCH /webhooks/:id/status
{ "site_id": "{{site_id}}", "token": "{{token}}", "status": "enabled" } |
Alerts
...
Emails and SMS are sent to notify webhook events stated above.
This are configured in the site configuration
Code Block |
---|
|
"on_failure": {
"contact_emails": [ //email alert recipients. In test environments emails are sent to test recipients set in Configuration/Outbound/Media/Test Emails
"email@company.com"
],
"contact_mobiles": [],
"sms_notification_name": "",
"email_notification_name": "webhook_failure" //do not change
},
"on_deactivation": {
"contact_emails": [ //email alert recipients. In test environments emails are sent to test recipients set in Configuration/Outbound/Media/Test Emails
"email@company.com"
],
"contact_mobiles": [],
"sms_notification_name": "",
"email_notification_name": "webhook_deactivation" //do not change
},
"on_failure_recovered": {
"contact_emails": [ //email alert recipients. In test environments emails are sent to test recipients set in Configuration/Outbound/Media/Test Emails
"email@company.com"
],
"contact_mobiles": [],
"sms_notification_name": "",
"email_notification_name": "webhook_failure_recovered" //do not change
} |
6. Webhook Statuses
enabled
: The default state after creation. Messages are processed normally, triggering the webhook.
Paused:
Messages are stored while the webhook is in a paused state. The behavior of this state depends on the scenario:
Manual pause: No messages will be sent until the webhook is manually re-enabled.
Automatic pause: This occurs when a message is not acknowledged by the receiver within 15 seconds. In this case, the message will be retried. If the message fails to be acknowledged after the configured number of retries (retries_until_failure
), an on_failure notification will be triggered (via email and/or SMS, depending on configuration). If the maximum number of retries is reached without success, the webhook will be disabled
. However, if the message is acknowledged before reaching the retry limit, the webhook will automatically resume sending messages (enabled
).
disabled
: The webhook stops functioning and storing messages after an on_deactivation
error. It must be manually reactivated.
7. Message storage
Messages are stored for a week; those older than a week are automatically deleted.
8. Integration Requirements
1. Handling Idempotency in Webhook Integration
When integrating with OneStock's webhook system, it is crucial that your webhook client handles idempotency. OneStock expects all webhook clients to process messages in an idempotent manner to ensure consistency and reliability.
What Is Idempotency?
Idempotency refers to the property of certain operations where executing them multiple times yields the same result as executing them once. In the context of webhooks, this means that if the same message is received more than once, processing it multiple times does not cause unintended side effects or data inconsistencies.
Why Is Idempotency Important?
Due to network issues or other transient problems, a message sent by OneStock might not be acknowledged by your system. In such cases, OneStock will retry sending the same message to ensure it has been received and processed.
If your webhook client does not handle idempotency:
Duplicate processing: The same operation might be performed multiple times (e.g., applying the same order change repeatedly), leading to inconsistent data or errors.
Integration breakdowns: Critical systems like your ERP might encounter conflicts or errors due to repeated operations, potentially disrupting business processes.
How to Handle Idempotency
Each webhook message sent by OneStock includes a unique message-id
in the HTTP headers. This identifier is essential for ensuring idempotent processing.
Implement Message Queueing and Asynchronous Processing
Acknowledge Immediately: Upon receiving a webhook message, your system should send an acknowledgement to OneStock immediately.
Enqueue the Message: Store the message-id
and the message payload in a reliable queue for asynchronous processing.
Process Asynchronously: Use a separate worker or process to consume messages from the queue and handle the idempotency checks.
Ensure Idempotency in Asynchronous Processing
Store processed message IDs: Maintain a persistent record of all processed message-id
s.
Check before processing: Before processing a message from the queue, check if its message-id
exists in your records.
Ensure Atomic Operations
To prevent race conditions:
Atomic transactions: Combine the message processing and the storage of the message-id
into a single atomic transaction.
Thread safety: If your application processes messages in parallel, ensure that access to the message ID store is thread-safe.
Example Implementation
Webhook Handler Function
Code Block |
---|
|
function handleWebhookRequest(request):
messageId = request.headers['message-id']
messageBody = request.body
// Acknowledge the message immediately
sendAcknowledgement()
// Enqueue the message for asynchronous processing
enqueueMessage(messageId, messageBody)
|
Asynchronous Message Processor
Code Block |
---|
|
function processQueuedMessage():
while true:
message = dequeueMessage()
if message is not null:
messageId = message.id
messageBody = message.body
if isMessageIdProcessed(messageId):
// Skip processing as this message has already been handled
continue
try:
beginTransaction()
// Process the message
processWebhookData(messageBody)
// Record the message-id as processed
storeProcessedMessageId(messageId)
commitTransaction()
except Exception as e:
rollbackTransaction()
logError(e)
// Optionally, re-enqueue the message or handle the failure accordingly
else:
// No messages in the queue, wait or sleep before retrying
wait()
|
Acknowledging Messages
Successful processing: Since you acknowledge messages immediately, ensure that your asynchronous processing handles failures internally without relying on OneStock to retry.
Failed processing: Implement retry mechanisms within your message processor for transient errors.
Network failures: Immediate acknowledgement reduces the chance of network issues causing message retries from OneStock.
Advantages of This Approach
Improved performance: Acknowledging immediately reduces response time and prevents timeouts.
Scalability: Asynchronous processing allows your system to handle a higher volume of messages efficiently.
Reliability: Queueing messages ensures they are not lost and can be retried in case of transient failures.
Summary
Handle retries gracefully: Design your webhook client to process duplicate messages without adverse effects.
Use message-id
for idempotency: Leverage the unique message-id
header provided by OneStock for tracking.
Process asynchronously: Acknowledge messages immediately and process them in a separate worker or process.
Prevent duplicate operations: Ensure that repeated messages do not cause inconsistent states or errors in your systems.
8. Integration Best practices
Use a Queuing System to Receive Messages
To ensure smooth and reliable communication, it's essential to acknowledge webhook messages swiftly—ideally within 15 seconds—by returning a 202 response code. This approach ensures that OneStock knows your system has received the message, allowing the process to continue without delays.
Why Use a Queuing System?
Immediate Acknowledgment: By decoupling message reception from processing, you can instantly acknowledge the receipt of the message. The queue receives and stores the message, allowing your system to process it asynchronously without holding up the response.
Enhanced Reliability: A queuing system provides a buffer, ensuring that even if your system is temporarily down or under heavy load, messages are not lost. They will be processed once your system is ready.
Easier Troubleshooting: Queuing services often come with tools that allow you to view and manage received messages, making it easier to diagnose and resolve any communication issues.
...
AWS SQS (Simple Queue Service)
Google Cloud Pub/Sub
Apache Kafka
...
Log Message Reception
If you choose not to use a queuing system, it's vital to implement robust logging of all received messages. Logging should include the message content and the response sent back to OneStock.
Benefits of Logging:
Monitoring and Auditing: Detailed logs provide a trail of received messages, which is crucial for monitoring system performance and ensuring that all messages are accounted for.
Incident Response: In the event of an issue, logs enable you to quickly identify and address the problem, minimizing downtime and maintaining communication integrity.
Compliance and Debugging: Logs can also serve as an essential resource for compliance and debugging purposes, helping you ensure that your system meets all necessary requirements and functions as expected.