The OneStock Order In Store module offers a seamless checkout experience with built-in integrations for leading Payment Service Providers (PSPs) such as Adyen, Stripe, Hipay.
To provide even greater flexibility, we introduce the Payment Connector. This feature empowers partners and clients to develop custom integrations with their preferred payment solutions while maintaining compatibility with OneStock’s standard checkout functionalities.
With the Payment Connector, you can implement the same robust payment options available in our official integrations:
Direct capture: Immediate payment capture at checkout.
Deferred payment: Authorize payment at checkout and capture funds upon dispatch.
Pay By Links: Generate secure payment links for remote transactions.
Returns and Refunds Management: Seamlessly handle returns and refunds within OneStock’s workflows.
To integrate a custom payment solution, the partner or client must implement the provided interface, detailed in the accompanying OpenAPI specification file :
This guide will walk you through the steps to successfully build, test, and deploy your custom payment integration with OneStock.
2. Guide
2.1. Direct Capture
This scenario demonstrates how to initiate a payment transaction that authorizes and captures the amount to be paid in a single flow.
In the Direct Capture process, OneStock initiates the transaction by sending a request to the PSP Connector. The connector acknowledges receipt of the transaction and forwards the capture request to the designated Payment Service Provider. Once the payment provider processes the transaction, the result is communicated back to the PSP Connector, which then updates OneStock with the final status of the payment.
The following sequence diagram illustrates the steps involved in the direct capture process:
2.2. Deferred payment
This scenario outlines the process for initiating a payment authorization without an immediate capture. The Deferred Payment method allows for payment authorization at checkout, with the actual capture occurring later, typically after the goods have been dispatched.
In this flow, OneStock initiates the transaction by sending an authorization request to the PSP Connector. The connector acknowledges receipt and forwards the authorization to the Payment Service Provider. Upon receiving the authorization result, the connector updates OneStock with the transaction status.
Once the items are shipped, OneStock will send a capture request for the authorized amount (or a partial amount). The PSP Connector must immediately acknowledge this request and subsequently update OneStock with the capture result.
The sequence diagram below illustrates the deferred payment process:
2.3. Pay By Link
The Pay By Link scenario allows customers to complete their payment transactions from their own devices. This is particularly useful for remote transactions or when customers prefer to finalize payments outside of the physical store.
In this process, OneStock initiates the transaction by requesting a payment link from the PSP Connector. The connector generates the link and sends it back to OneStock, which then shares it with the customer.
The Payment Connector allows you to send a url to display as a QR code in the Order In Store. The link can also be send to the customer with a text and/or email notification.
The customer completes the payment through the provided link, and the PSP processes the transaction.
Depending on the payment type, this transaction may result in either a direct capture or a deferred payment (authorization only). OneStock expects to receive updates on the transaction status accordingly.
The sequence diagram below outlines the Pay By Link process:
2.4. Refunds
If refunds configured in the workflows with a payment execution of type refund, OneStock will call the connector in defferent use cases:
refund route for direct capture or deferred payments that have been captured
cancel authorisation for deferred payments that haven’t been captured
3. Develop your integration
3.1. Add a connection in your environment
To add a new connection to your environment we will be asking few questions:
URL of your connector : please provide a url for production and development. We recommend your developers to use a solution such as ngrok to develop the connector before hosting it in the cloud.
Name of the connector
Image of the connector : base64 image
For connectors implementing a Pay By Link scenario
expiration : duration of the expiration
format : format value of the data response to display in the Order In Store checkout. Possible values are
qrcode
barcode
text
url
image
barcode.format : for a barcode result, specify the barcode format. Available values are
CODE128
CODE128A
CODE128B
CODE128C
EAN13
UPC
EAN8
EAN5
EAN2
We will then share the hash that will be used for signing all the API calls between OneStock and your connector
3.2. Security - check the OneStock signature and sign your calls
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}`);
API response best practice
To avoid unnecessary processing it is crucial to focus on the signature validation first
For asynchronous content validation (if applicable): any additional checks should be performed asynchronously after acknowledging the message.
Signing a call to our APIs
To update a transaction, you will have to call either
POST external_payments/authorisation_update
POST /external_payments/capture_update
POST /external_payments/refund_update
You will have to add the Onestock-signature header to those call. Find bellow a pre-request script for Postman to create your signature using the CryptoJS library, assuming that you have saved the secret hash in an environment variable hashkey.
3.3 Adding payment terminals in store
Once the connector is setup, the payment terminals can be added for every stores directly from your OMC. It is up to the client to correctly setup those terminals depending on the connector specifications:
For a connector implementing direct captures or deferred captures, a POID of the terminal will be required for every stores
For a connector implementing a Pay By Link, you will be creating a virtual terminal
4. FAQ
4.1. Debug API call
API calls are currently accessible only via API. Your OneStock contact will give you the details to access the payment logs that will be displayed in the order details in a future release.
4.2. Use case : Take payment from a POS
A client might want to use our Order In Store for queue busting but doesn’t want to contract with our PSP partners.
They can implement the payment connector to bridge the payment to their POS if their POS allows it.
Creating a payment connector
Implement the Pay By Link workflow above
Pay By Link can answer a data that we display in our checkout screen in a barcode format that the POS can scan to take the payment
4.3. Ressources
You can find bellow a markdown file with the sequence diagrams that have to be implemented to be a starting point for your project specification to add specific requirements for every connection you wish to build.