Trustly API calls are secured by cryptographic signatures included on all request payloads and event notifications. Request signatures are required for production API requests and SDK operations. Additionally, Trustly strongly recommends that applications verify the signatures included in redirect notifications from Trustly UI SDKs and incoming webhook notifications.

📘

Example Code

Check out Trustly's example Nestjs app for a working demonstration of how to generate and handle request signatures and integrate them with your frontend application.

Overview

This page covers several areas where additional security can be utilized with the Trustly API.

  • Generating Request Signatures to include in SDK functions and server-side API calls
  • Validating signatures on SDK redirect functions
  • Validating signatures on event notifications
  • Encrypting sensitive values before sending to Trustly

Generating Request Signatures

A request signature is required in the Establish Data used by the Trustly UI SDK functions and the POST /establish API endpoint to ensure the integrity of the transaction offering robust protection against any potential tampering. Generating the requestSignature should be done server-side as your accessKey should not be exposed in any client side code. The requestSignature is a Base64 HMAC-SHA1 encoded hash of the payment data in a request payload using your accessKey.

To create a requestSignature, follow these steps:

  1. Flatten the JSON request to a string.
  2. Using your accessKey and the flattened string, calculate the signature and generate a Base64 encoded HMAC-SHA1 hash.
  3. Add the encoded string as the value of the requestSignature parameter in your request.

🚧

Note on Ordering

Any fields included in the request payload must be included in the requestSignature and in the following order. However, fields listed below which are not included in the request payload should also be omitted from the requestSignature.

  • accessId
  • merchantId
  • description
  • currency
  • amount
  • displayAmount
  • minimumBalance
  • merchantReference
  • paymentType
  • timeZone
  • recurrence.startDate
  • recurrence.endDate
  • recurrence.frequency
  • recurrence.frequencyUnit
  • recurrence.frequencyUnitType
  • recurrence.recurringAmount
  • recurrence.automaticCapture
  • verification.status
  • verification.verifyCustomer
  • customer.customerId
  • customer.externalId
  • customer.name
  • customer.vip
  • customer.taxId
  • customer.driverLicense.number
  • customer.driverLicense.state
  • customer.address.address1
  • customer.address.address2
  • customer.address.city
  • customer.address.state
  • customer.address.zip
  • customer.address.country
  • customer.phone
  • customer.email
  • customer.balance
  • customer.currency
  • customer.enrollDate
  • customer.externalTier
  • customer.dateOfBirth
  • account.nameOnAccount
  • account.name
  • account.type
  • account.profile
  • account.accountNumber
  • account.routingNumber
  • transactionId

A functional example of this logic can be found here in Trustly's example NestJS app

const Crypto = require ('crypto');
const generateSignature = (establishData, accessKey) => {
  let query = '';
  query += `accessId=${establishData.accessId}`;
  query += `&merchantId=${establishData.merchantId}`;
  query += `&description=${establishData.description}`;
  query += `&currency=${establishData.currency}`;
  query += `&amount=${establishData.amount}`;
  
  if (establishData.displayAmount) query += `&displayAmount=${establishData.displayAmount}`;
  if (establishData.minimumBalance) query += `&minimumBalance=${establishData.minimumBalance}`;
  
  query += `&merchantReference=${establishData.merchantReference}`;
  query += `&paymentType=${establishData.paymentType}`;
  
  if (establishData.timeZone) query += `&timeZone=${establishData.timeZone}`;

  if (establishData.paymentType === 'Recurring' && establishData.recurrence) {
    if (establishData.recurrence.startDate) query += `&recurrence.startDate=${establishData.recurrence.startDate}`;
    if (establishData.recurrence.endDate) query += `&recurrence.endDate=${establishData.recurrence.endDate}`;
    if (establishData.recurrence.frequency) query += `&recurrence.frequency=${establishData.recurrence.frequency}`;
    if (establishData.recurrence.frequencyUnit) query += `&recurrence.frequencyUnit=${establishData.recurrence.frequencyUnit}`;
    if (establishData.recurrence.frequencyUnitType) query += `&recurrence.frequencyUnitType=${establishData.recurrence.frequencyUnitType}`;
    if (establishData.recurrence.recurringAmount) query += `&recurrence.recurringAmount=${establishData.recurrence.recurringAmount}`;
    if (establishData.recurrence.automaticCapture) query += `&recurrence.automaticCapture=${establishData.recurrence.automaticCapture}`;
  }

  if (establishData.verification) {
    if (establishData.verification.status) query += `&verification.status=${establishData.verification.status}`;
    if (establishData.verification.verifyCustomer) query += `&verification.verifyCustomer=${establishData.verification.verifyCustomer}`;
  }

  if (establishData.customer) {
    if (establishData.customer.customerId) query += `&customer.customerId=${establishData.customer.customerId}`;
    if (establishData.customer.externalId) query += `&customer.externalId=${establishData.customer.externalId}`;
    if (establishData.customer.name) query += `&customer.name=${establishData.customer.name}`;
    if (establishData.customer.vip !== undefined) query += `&customer.vip=${establishData.customer.vip}`;
    if (establishData.customer.taxId) query += `&customer.taxId=${establishData.customer.taxId}`;
    if (establishData.customer.driverLicense) {
      if (establishData.customer.driverLicense.number) query += `&customer.driverLicense.number=${establishData.customer.driverLicense.number}`;
      if (establishData.customer.driverLicense.state) query += `&customer.driverLicense.state=${establishData.customer.driverLicense.state}`;
    }
    if (establishData.customer.address) {
      if (establishData.customer.address.address1) query += `&customer.address.address1=${establishData.customer.address.address1}`;
      if (establishData.customer.address.address2) query += `&customer.address.address2=${establishData.customer.address.address2}`;
      if (establishData.customer.address.city) query += `&customer.address.city=${establishData.customer.address.city}`;
      if (establishData.customer.address.state) query += `&customer.address.state=${establishData.customer.address.state}`;
      if (establishData.customer.address.zip) query += `&customer.address.zip=${establishData.customer.address.zip}`;
      if (establishData.customer.address.country) query += `&customer.address.country=${establishData.customer.address.country}`;
    }
    if (establishData.customer.phone) query += `&customer.phone=${establishData.customer.phone}`;
    if (establishData.customer.email) query += `&customer.email=${establishData.customer.email}`;
    if (establishData.customer.balance) query += `&customer.balance=${establishData.customer.balance}`;
    if (establishData.customer.currency) query += `&customer.currency=${establishData.customer.currency}`;
    if (establishData.customer.enrollDate) query += `&customer.enrollDate=${establishData.customer.enrollDate}`;
 if (establishData.customer.dateOfBirth) query += `&customer.dateOfBirth=${establishData.customer.dateOfBirth}`;
  }

  if (establishData.account) {
    if (establishData.account.nameOnAccount) query += `&account.nameOnAccount=${establishData.account.nameOnAccount}`;
    if (establishData.account.name) query += `&account.name=${establishData.account.name}`;
    if (establishData.account.type) query += `&account.type=${establishData.account.type}`;
    if (establishData.account.profile) query += `&account.profile=${establishData.account.profile}`;
    if (establishData.account.accountNumber) query += `&account.accountNumber=${establishData.account.accountNumber}`;
    if (establishData.account.routingNumber) query += `&account.routingNumber=${establishData.account.routingNumber}`;
  }

  if (establishData.transactionId) query += `&transactionId=${establishData.transactionId}`;

  const requestSignature = Crypto.createHmac('sha1', accessKey).update(query).digest('base64');
  return requestSignature;
}
public class TrustlySignature {

  public static class Account { //Used only on account verification
    public String nameOnAccount;
    public String name;
    public String type;
    public String profile;
    public String accountNumber;
    public String routingNumber;
  }

  public static class Recurrence { //Used only when payment type is Recurring
    public Date startDate;
    public Date endDate;
    public Integer frequency;
    public Integer frequencyUnit;
    public Integer frequencyUnitType;
    public BigDecimal recurringAmount;
    public Boolean automaticCapture;
  }

  public static class Address {
    public String address1;
    public String address2;
    public String city;
    public String state;
    public String zip;
    public String country;
  }

  public static class DriverLicense {
    public String number;
    public String state;
  }

  public static class Customer {
    public String customerId; //Trustly's customer ID
    public String externalId; //Merchant’s customer ID
    public String name;
    public String vip;
    public String taxId;
    public DriverLicense dL;
    public Address address;
    public String phone;
    public String email;
    public String balance;
    public String currency;
    public String enrollDate;
    public String dateOfBirth;
  }

  public static class Verification {
    public String status;
    public Boolean verifyCustomer;
  }

  public String createRequestSignature(String accessId,
                                       String accessKey,
                                       String merchantId,
                                       String description,
                                       String currency,
                                       BigDecimal amount,
                                       BigDecimal displayAmount,
                                       BigDecimal minimumBalance,
                                       String merchantReference,
                                       String paymentType,
                                       String timeZone,
                                       Recurrence r,
                                       Verification v,
                                       Customer c,
                                       Account a,
                                       String transactionId) throws Exception {
    String str = "accessId="+ accessId;
		str += "&merchantId="+merchantId;
    str += "&description="+description;
    str += "&currency="+currency;
    str += "&amount="+amount.setScale(2); //2 decimal places such as 13.25
    if(displayAmount != null) str += "&displayAmount="+displayAmount.setScale(2); //2 decimal places such as 13.25
    if(minimumBalance != null) str += "&minimumBalance="+minimumBalance.setScale(2); //2 decimal places such as 13.25
    str += "&merchantReference="+merchantReference;
    str += "&paymentType="+paymentType;
    if(timeZone != null) str += "&timeZone="+timeZone;

    if("Recurring".equals(paymentType) && r != null){
      if(r.startDate != null) str += "&recurrence.startDate=" + r.startDate.getTime();
      if(r.endDate != null) str += "&recurrence.endDate=" + r.endDate.getTime();
      if(r.frequency != null) str += "&recurrence.frequency="+r.frequency;
      if(r.frequencyUnit != null) str += "&recurrence.frequencyUnit="+r.frequencyUnit;
      if(r.frequencyUnitType != null) str +="&recurrence.frequencyUnitType="+r.frequencyUnitType;
      if(r.recurringAmount != null) str +="&recurrence.recurringAmount="+r.recurringAmount;
      if(r.automaticCapture != null) str +="&recurrence.automaticCapture="+r.automaticCapture;
    }

    if (v != null) {
      if(v.status != null) str +="&verification.status=" + v.status;
      if(v.verifyCustomer != null) str +="&verification.verifyCustomer=" + v.verifyCustomer;      
    }

    if(c != null) {
      if(c.customerId != null) str +="&customer.customerId=" + c.customerId;
      if(c.externalId != null) str +="&customer.externalId=" + c.externalId;
      if(c.name != null) str +="&customer.name=" + c.name;
      if(c.vip != null) str +="&customer.vip=" + c.vip;
      if(c.taxId != null) str +="&customer.taxId=" + c.taxId;

      if(c.dL != null) {
	      if(c.dL.number != null) str +="&customer.driverLicense.number=" + c.dL.number;
        if(c.dL.state != null) str +="&customer.driverLicense.state=" + c.dL.state;
      }

      if(c.address != null) {
        if(c.address.address1 != null) str +="&customer.address.address1=" + c.address.address1;
        if(c.address.address2 != null) str +="&customer.address.address2=" + c.address.address2;
        if(c.address.city != null) str +="&customer.address.city=" + c.address.city;
        if(c.address.state != null) str +="&customer.address.state=" + c.address.state;
        if(c.address.zip != null) str +="&customer.address.zip=" + c.address.zip;
        if(c.address.country != null) str +="&customer.address.country=" + c.address.country;
      }
      if(c.phone != null) str +="&customer.phone=" + c.phone;
      if(c.email != null) str +="&customer.email=" + c.email;
      if(c.balance != null) str +="&customer.balance=" + c.balance.setScale(2);      
      if(c.currency != null) str +="&customer.currency=" + c.currency;
      if(c.enrollDate != null) str +="&customer.enrollDate=" + c.enrollDate;
      if(c.dateOfBirth != null) str +="&customer.dateOfBirth=" + c.dateOfBirth;
    }

    if(a != null) {
      if(a.nameOnAccount != null) str +="account.nameOnAccount=" + a.nameOnAccount;
      if(a.name != null) str +="account.name=" + a.name;
      if(a.type != null) str +="account.type=" + a.type;
      if(a.accountNumber!= null) str +="account.accountNumber=" + a.accountNumber;
      if(a.routingNumber!= null) str +="account.routingNumber=" + a.routingNumber;
    }

    if(transactionId != null) str += "&transactionId="+transactionId;

    return TrustlySignature.doHashing(str,accessKey);
  }

  public static String doHashing(String str, String accessKey) throws Exception {
    Mac = Mac.getInstance("HmacSHA1");
    SecretKeySpec signingKey = new SecretKeySpec(accessKey.getBytes(),"HmacSHA1");
    mac.init(signingKey);
    byte[] rawHmac = mac.doFinal(str.getBytes());
    return encodeBase64(rawHmac);
  }

  public static String encodeBase64(byte[] binary){
    return DatatypeConverter.printBase64Binary(binary);
  }
}

Validate the Redirect Signature

When handling a redirect notification from the Lightbox SDK, you can check the validity of the notification by calculating the requestSignature and comparing it to the value included on the notification payload.

To calculate a requestSignature, follow these steps:

Given the following returnUrl:

https://merchant.com/Trustly/return?transactionId=1002655801&transactionType=1&merchantReference=123123&status=2&payment.paymentType=4&payment.paymentProvider.type=1&payment.account.verified=false&panel=1&requestSignature=2uvoRTIEFWzfsbZwXokudh5P5rs%3D&instantPayoutAvail=true

1. Remove the requestSignature parameter (including &) and it's value from the string.

https://merchant.com/Trustly/return?transactionId=1002655801&transactionType=1&merchantReference=123123&status=2&payment.paymentType=4&payment.paymentProvider.type=1&payment.account.verified=false&panel=1

🚧

Check API Version

For API versions below 1.180.0, the requestSignature included in the payload sent to the provided returnUrl endpoint is calculated using ONLY the query parameters. Remove the base url and paths from the string above before proceeding to step 2.

For API versions below 1.170.0 the requestSignature included in the payload sent to the provided cancelUrl endpoint is calculated using ONLY the query parameters. Remove the base url and paths from the string above before proceeding to step 2.

2. Using your accessKey, calculate the signature and generate a Base64 encoded HMAC-SHA1 hash.

2uvoRTIEFWzfsbZwXokudh5P5rs=

3. Compare the calculated hash (2uvoRTIEFWzfsbZwXokudh5P5rs=) to the decoded hash that was passed in the query string (2uvoRTIEFWzfsbZwXokudh5P5rs=).

Validate the Notification Signature

Validate the Notification Signature

const decodeURI = (encodedString) => {
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url
  // decodeURIComponent cannot be used directly to parse query parameters from a URL. It needs a bit of preparation
  return decodeURIComponent(encodedString.replace(/\+/g, ' '));
}

const isValidSignature = (request, header, apiKey) => {
  const test = Crypto.createHmac('sha1', apiKey).update(decodeURI(request)).digest('base64');
  const auth = Buffer.from(header, 'base64').toString('utf8').split(':');
  return test === auth[1];
}

The request signature is calculated as a HMAC-SHA1 of the request parameters using accessKey as the signing key. To calculate, follow these steps:

1. Read the entire POST request body.

merchantId=1002463580&merchantReference=cb180040-7210-4ab9-97b7-415824754802&paymentType=2&transactionType=3&eventId=1002593570&eventType=Authorize&objectId=1002593555&objectType=Transaction&message=&timeZone=Etc%2FUTC&createdAt=1556234040954&accessId=M8RaHgEjBE54zuFYMRQq&paymentProviderTransaction.status=AC100&paymentProviderTransaction.statusMessage=AC100&status=2&statusMessage=Authorized

2. Decode the string using UTF-8.

merchantId=1002463580&merchantReference=cb180040-7210-4ab9-97b7-415824754802&paymentType=2&transactionType=3&eventId=1002593570&eventType=Authorize&objectId=1002593555&objectType=Transaction&message=&timeZone=Etc/UTC&createdAt=1556234040954&accessId=M8RaHgEjBE54zuFYMRQq&paymentProviderTransaction.status=AC100&paymentProviderTransaction.statusMessage=AC100&status=2&statusMessage=Authorized

3. Using your accessKey, calculate the signature and generate a Base64 encoded HMAC-SHA1 hash.

EYN3GXasrVU1vQ1uyYz22NNQdy4=

4. Grab the Authorization header and remove the Basic prefix.

TThSYUhnRWpCRTU0enVGWU1SUXE6RVlOM0dYYXNyVlUxdlExdXlZejIyTk5RZHk0PQ==

5. Decode the Base64 string to get a accessId:signature string.

M8RaHgEjBE54zuFYMRQq:EYN3GXasrVU1vQ1uyYz22NNQdy4=

6. Split the decoded string on the : to the get the signature.

EYN3GXasrVU1vQ1uyYz22NNQdy4=

  1. Compare the values calculated in Steps 3 (EYN3GXasrVU1vQ1uyYz22NNQdy4=) and 6 (EYN3GXasrVU1vQ1uyYz22NNQdy4=). If they match, the request is valid. If they do not match and the failures continue for a long period of time, contact Trustly.

You can use the example Header and Request above, with an accessKey of vMBWAvMXdPM27F9qZEkr to confirm your signature verification code is working properly.

Encrypt a field value

const Crypto = require('crypto');

const accessKey = 'YOUR_ACCESS_KEY';
const attributeValue = '123123456';

const algorithm = 'aes-256-cbc';
const encoding = 'base64';

const iv = Buffer.from(Crypto.randomBytes(16)).toString('hex').slice(0, 16);;

const keyHash = Crypto.createHash('sha256').update(accessKey).digest();

const cipher = Crypto.createCipheriv(algorithm, keyHash.slice(0, 32), iv);
cipher.setAutoPadding(true);

let encrypted = cipher.update(iv + attributeValue, 'utf8', encoding);
encrypted += cipher.final(encoding);

console.log('response', 'crypt2:' + encrypted);
public class EncryptedAttribute {
    public static final String AES_ECB_PKCS_5_PADDING = "AES/ECB/PKCS5Padding";
    private static volatile SecureRandom numberGenerator = new SecureRandom();
    public static byte[] salt(byte[] data) {
        byte[] salt = new byte[4];
        numberGenerator.nextBytes(salt);
        byte[] salted = new byte[data.length + salt.length];
        System.arraycopy(salt, 0, salted, 0, salt.length);
        System.arraycopy(data, 0, salted, salt.length, data.length);
        return salted;
    }
    public static byte[] unsalt(byte[] data) {
        byte[] unsalted = new byte[data.length - 4];
        System.arraycopy(data, 4, unsalted, 0, unsalted.length);
        return unsalted;
    }
    public static String encodeBase64(byte[] binary) {
        return Base64.getEncoder().encodeToString(binary);
    }
    public static byte[] decodeBase64(String str) {
        try {
            return Base64.getDecoder().decode(str);
        } catch (Exception e) {
            return new byte[0];
        }
    }
    public static String encryptAttribute(String attribute, String accessKey) throws Exception {
        if (attribute == null || attribute.length() == 0) {
            return attribute;
        }
        Cipher cipher = Cipher.getInstance(AES_ECB_PKCS_5_PADDING);
        cipher.init(Cipher.ENCRYPT_MODE, buildKey(accessKey));
        return "crypt:" + encodeBase64(cipher.doFinal(salt(attribute.getBytes("UTF-8"))));
    }
    public static String decryptAttribute(String attribute, String accessKey) {
        if (attribute == null || accessKey == null || !attribute.startsWith("crypt2:")) { //If attribute is not
            encrypted
            return it
            return attribute;
        }
        try {
            Cipher cipher = Cipher.getInstance(AES_ECB_PKCS_5_PADDING);
            cipher.init(Cipher.DECRYPT_MODE, buildKey(accessKey));
            return new String(unsalt(cipher.doFinal(decodeBase64(attribute.substring("crypt2:".length())))), "UTF-
                8 ");
            }
            catch (Exception e) {
                return attribute;
            }
        }
        private static SecretKeySpec buildKey(String accessKey) throws Exception {
            MessageDigest digester = MessageDigest.getInstance("SHA-256");
            digester.update(accessKey.getBytes("UTF-8"));
            byte[] key = Arrays.copyOf(digester.digest(), 32);
            SecretKeySpec spec = new SecretKeySpec(key, "AES");
            return spec;
        }
    }
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

public class Program
{
	public static void Main()
	{
		const string accessKey = "YOUR_ACCESS_KEY";
		const string attributeValue = "123123456";

		var random = new Byte[16];
		RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
		rng.GetBytes(random);
		var iv = BitConverter.ToString(random).Replace("-", "").Substring(0, 16);
		
		var keyHash = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(accessKey));
		
        Aes aes = Aes.Create();
        aes.Key = keyHash;
        aes.IV = UTF8Encoding.UTF8.GetBytes(iv);
        aes.Mode = CipherMode.CBC;
        ICryptoTransform cipher = aes.CreateEncryptor(aes.Key, aes.IV);
		
		var inputBuffer = aes.IV.Concat(UTF8Encoding.UTF8.GetBytes(attributeValue)).ToArray();
		var resultArray = cipher.TransformFinalBlock(inputBuffer, 0, inputBuffer.Length);
		
        var encrypted = Convert.ToBase64String(resultArray, 0, resultArray.Length);
		Console.WriteLine("crypt2:" + encrypted);
	}
}

It is possible to encrypt the attributes sent on the establish data object to Trustly. The encryption is done using the AES-256-CBC algorithm and your Trustly accessKey as the encryption key. The encrypted attributes start with the prefix “crypt2:” and the full encrypted value (including the prefix) must be used when calculating the request signature.

1. Create an initialization vector (iv) containing 16 random characters.

2. Create a SHA256 hash of your accessKey.

3. Using the first 32 characters of your accessKey hash (from step 2) and the initialization vector (from step 1), create a cipher.

4. Using the cipher key (from step 3), the initialization vector (from step 1), and the attribute value to encrypt, update the cipher.

5. Create an encrypted string by finalizing the cipher using base64 encoding.

6. Concatenate crypt2: and your encrypted string, and pass the value in the establishData value that is passed to the SDK:

customer: {
  name: 'John Smith',
  taxId: 'crypt2:uFVg4qGHj7ZtwSv1tkFAL7pBJ5x8zsehYgNdU51w5yA=',
  address: {
    country: 'US',
  }
},