Signing
Verify that an exchange request originates from a TGU or GMS owner server
By signing our exchanges, you can verify the origin via a signature in the headers. This ensures the validity of the data in the request without needing an API call to retrieve the status of the object.
To prevent valid signatures from failing due to a different order of JSON elements, ensure that you verify the request before performing any deserialization or normalization actions.
Configure the right Exchange Request method
You can enable exchange signing in the Service Location edit screen in the Global Management System. To enable signing you need to use method: "SIGNED JSON POST" with ID: "4'.


How to verify an exchange sent by the platform
To verify the signature of an exchange, follow these steps:
- Retrieve the headers and request body.
- Determine the signing method from the
signature-method
header. - Fetch the secret corresponding to the key used for signing, based on the
signature-keyid
header.- if the
signature-keyid
starts with SL then you need to get the secret from the sales location. In my.pay.nl navigate to Settings > Sales location and get the secret from the right sales location code. - if the
signature-keyid
starts with AT then you need to get token (API-key). In my.pay.nl navigate to Merchant > API tokens and get the token from the right AT-code.
- if the
- Perform signature verification using the determined signing method.
- For HMAC verification see: Verifying a HMAC signed exchange
- By completing these steps, you can determine if the request was sent by the the platform.
GitHub
Check out our library for signing and verifying requests and exchange calls on GitHub
Example signed request
The URL query parameters (GET) in the request are for logging purposes only and are not considered safe.
https://<your_hostname>/ipn/?type=order&id=657c206e-9969-8d9f-11cd-737862306956&event=completed
Headers
A signed exchange contains all the necessary data in its headers for verification. To verify the exchange, you will only need the secret associated with the shared key. We provide the following headers for your use:
signature-algorithm
: Specifies the algorithm used for signature calculation.signature-method
: Indicates the method used for signing the exchange. Currently supports HMAC.signature
: Represents the actual signature.signature-keyid
: Identifies the key used for signing. You can retrieve your corresponding secret using this ID to verify our exchange.
--header 'signature-algorithm: SHA256' \
--header 'signature-method: HMAC' \
--header 'signature: 9c64d246ffbd15de0c8863ec0dea1ea419208eed46ec3879296ba7a72f791ce6' \
--header 'signature-keyid: SL-1234-1234' \
Verifying a HMAC signed exchange
The method used to sign an exchange depends on how the object was created in our system:
- If the object was created using your AT code, we will use the corresponding secret associated with it to sign the request.
- If the object was created using your SL code, we will use the corresponding secret associated with it to sign the request.
To fill in the blanks in the instructions for verifying a signature using our HMAC signing method in the "How to verify an exchange sent by the TGU" section follow these steps:
- Use the algorithm specified in the
signature-algorithm
header to hash this string to hash the body of the request using the HMAC algorithm. - Compare the generated signature string, preferably using a time safe comparison method, with the one provided in the Signature header. If they match, the signature is valid. If they differ, mark the exchange as FAILED.
There are two way on how you can fill in these blanks:
Using our PHP request-signing package
To simplify the process of signing and verifying requests in a specific language, we offer a public package. By using this package, you can avoid the need to write the logic from scratch. Currently, we provide a package specifically designed for the PHP language: https://github.com/paynl/request-signing.
Writing the verifying code yourself
The following code examples demonstrate a simplified and concise approach to verify an exchange by chaining all the necessary steps together:
$headers = getallheaders();
$rawBody = file_get_contents('php://input');
$signingMethod = $headers['signature-method'] ?? null;
$processingSucceeded = false;
$isValid = false;
$returnMessage = null;
try {
if ($signingMethod === 'HMAC') {
// Find your key based on the KeyID header.
// Note: These are dummy keys and not actual production keys.
$secret = match ($headers['signature-keyid']) {
'SL-1234-1234' => '7b7b7e5e4448e491bfbf5202bc97ba7205ddfe4c',
'AT-1234-1234' => 'd537b145ac3650b99c3607273f89e97b77b78311',
default => throw new Exception('Key not found'),
};
$signature = hash_hmac($headers['signature-algorithm'] ?? 'sha256', $rawBody, $secret);
$isValid = hash_equals($headers['signature'] ?? '', $signature);
}
} catch(Throwable $throwable) {
$isValid = false;
}
if ($isValid === true) {
// Your logic on handling the exchange as at this point the request is validated to be correct
$data = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
$exchangeType = $data['type'] ?? null;
if ($exchangeType === 'order') {
$exchangedOrder = $data['object'];
// Fetch the order from your database
$internalOrder = $this->db->fetchOrderById($exchangedOrder['id']);
// Retrieve the order status from the exchange and map it to your own status
$orderStatus = match($exchangedOrder['status']['code']) {
100 => 'PAID',
95 => 'AUTHORIZED',
50 => 'PENDING',
20 => 'CREATED',
-60 => 'FAILED',
-80 => 'EXPIRED',
-90 => 'CANCELLED'
};
// Update your order if needed
if ($internalOrder->getStatus() !== $orderStatus) {
$internalOrder->setStatus($orderStatus);
$returnMessage = sprintf('Order status progressed to [%s]', $orderStatus);
} else {
$returnMessage = sprintf('Order was already in status [%s], no action needed.', $orderStatus);
}
if($this->db->saveOrder($internalOrder)) {
$processingSucceeded = true;
}
}
}
echo json_encode([
'result' => $isValid,
'description' => $isValid ? 'Signature OK' : 'Signature verification failed'
]);
exit;
This code combines all the required steps to verify an exchange in the simplest manner. It takes the key ID, secret, raw body, signature algorithm, and received signature as input. It generates a signature by hashing the concatenated string using the specified algorithm. Finally, it compares the generated signature with the received signature and returns True if they match, indicating a successful verification, or False otherwise.
Sample signed exchanges
TGU Order exchange
Exchanges send for orders started with the Order:Create API's on a TGU will have a signed exchange that looks as follows:
Alternative response for Transaction Actions not performed on the TGUThe
Refund API
is not performed on the TGU. This means that the response body will differ. Make sure that you check the type of response body to properly handle the action.
--header 'signature-algorithm: SHA512' \
--header 'signature-method: HMAC' \
--header 'signature: 9c64d246ffbd15de0c8863ec0dea1ea419208eed46ec3879296ba7a72f791ce6' \
--header 'signature-keyid: SL-1234-1234' \
--data {
"event": "status_changed",
"type": "order",
"version": 1,
"id": "4f82bbef-9f8c-4db1-8e49-fb7d38bd3ebc",
"object": {
"id": "4f82bbef-9f8c-4db1-8e49-fb7d38bd3ebc",
"type": "sale",
"serviceId": "SL-1234-1234",
"description": "Order description",
"reference": "REF1234",
"manualTransferCode": "6000 0510 0440 0025",
"orderId": "00000000000X0000",
"uuid": "4f82bbef-9f8c-4db1-8e49-fb7d38bd3ebc",
"status": {
"code": 100,
"action": "PAID"
},
"receipt": null,
"integration": {
"test": false
},
"amount": {
"value": 3,
"currency": "EUR"
},
"paidAmount": {
"value": 0,
"currency": "EUR"
},
"payments": [
{
"id": "67aa80e6-cc8f-4861-940c-b104d8b73c6d",
"status": {
"action": "PAID",
"code": 100
},
"paymentMethod": {
"id": 10,
"subId": "4"
},
"amount": {
"currency": "EUR",
"value": 1
},
"currencyAmount": {
"currency": "EUR",
"value": 1
},
"customerId": "<customer_id>",
"customerKey": "<customer_key>",
"customerName": "<customer_name>",
"customerType": "<customer_type>",
"ipAddress": "127.0.0.1",
"paymentVerificationMethod": 0,
"secureStatus": false
}
],
"createdAt": "2023-11-23T11:43:13+01:00",
"createdBy": "SL-1234-1234",
"modifiedAt": "2023-11-23T11:43:13+01:00",
"modifiedBy": "TGU 123456",
"expiresAt": "2023-12-21T11:43:13+01:00",
"completedAt": "2023-12-21T11:45:13+01:00",
"links": {
"cancel": "<cancel_url>",
"status": "<status_url>",
"redirect": "<redirect_url>"
}
}
}
Updated 3 months ago