Customer Relation Manager (CRM) for Order In Store

Customer Relation Manager (CRM) for Order In Store

 

1. Introduction

Our Order In store is shipped with our in house Customer Relation Manager that allows store managers to quickly search for an existing customer who has already made a purchase with our Order In Store.

For a real omnichannel experience, searching for an existing client directly from an third party CRM is a must have, so that you can quickly find an online or a loyal in store customer.

2. Documentation

To integrate a CRM, the partner or client must implement the following interface, detailed in this OpenAPI specification file :

In order for our Order In Store checkout to work smoothly, you will have to implement a middleware to your CRM so that we can perform different operations.

  • search, create and edit a customer

  • create and edit a customer’s address

Search, create and edit a customer

 

image-20250729-131915.png

 

Create, edit a customer’s address

 

image-20250729-141711.png

3. Guide

3.1. Add a CRM for your saleschannel

A configuration page is still in progress so that any configuration manager can add a CRM integration

To add a CRM for your order in store saleschannel, you can ask your OneStock contact to enable it with some information:

  • URL : url of the API interface that you are implementing

  • TestURL : url of the API interface for your developer. We recommend your developers to use a solution such as ngrok to develop the connector before hosting it in the cloud.

    • The TestURL takes priority over the URL and will therefore be used as the main URL. It must be deleted for the URL property to be taken into account.

    • It will not be possible to configure TestURL in production.

  • Name : name of the connector

In return your OneStock contact will share a HashKey so that your integration can verify the signature of all the API calls that OneStock will do to your CRM (see bellow).

3.2. When are the different apis are called in our Order In Store ?

To trigger the different APIs you will need a environment with our Catalogue with Order activated.

image-20250729-133704.png
First order a product and go to payment
  1. The search input will trigger the POST /customers/query request

  2. Create customer button will trigger the POST /customers request

 

 

  1. Selecting a customer from the query will trigger the GET /customers/{customerId} request

  1. Editing and submitting a customer information will trigger the PATCH /customers/{id} request

  2. Editing and submitting a billing or delivery address will trigger the PATCH /customers/{customerId}/addresses/{addressId} request

3.3. Security - check the OneStock signature

All API calls between OneStock and the connectors must to be signed.

Each API call includes a signature in the header with the following format: timestamp + "," + signature.

During the connector setup a hash key is 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 in second

  • 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.

Encryption : the encryption is a HMAC in SHA256 of the computed string : timestamp and the requestBody seperated by a . character.

Exemple of signature and verification code

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

<?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; // 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."; } ?>
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)): 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. :param secret_key: The secret key as a string. :param payload: The payload to hash, consisting of the timestamp and request body. :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.")
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 current key. if (checkWebhookSignature(req, key)) { // If verification succeeds with the current key, return true immediately. return true; } } // 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 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}`);
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

API response best practice

To avoid unnecessary processing it is crucial to focus on the signature validation first