Skip to main content

Creating Charges - Safe Acceptance: Signature Generation and Validation

Safe Acceptance - Signature Generation and Validation

Why is a signature necessary when sending a request via the Xendit Safe Acceptance API? 

Using this API, requests are initiated, and responses returned to, the client side instead of server-to-server.  This exposes a possible security vulnerability if the requests/responses are intercepted during communication, and the interceptor interferes with the payload. 

The API uses a signature as a validator for requests and corresponding responses.  A signature is generated from the request and response body by hashing with a shared secret key that is only known by the merchant and Xendit.

If you verify the signature in Xendit’s response and it does not match your request body, then it is likely that the response has been tampered with and potentially exposed to fraud.  The response should be rejected in that situation.

How to create a signature

Steps required:

  1. Generate the shared secret from your secret API key
  2. Generate a string representation of your transaction body
  3. Sign the string with your shared secret
  4. Add the signature and signed_fields to the form that will be POST-ed to the safe acceptance API

1. Generate the shared secret from your secret API key

To create the shared secret, use the any secret API key with write permission generated from the Xendit Dashboard and hash it with sha256

const sharedSecret = crypto.createHash('sha256')
.update('put_your_Xendit_secret_API_key_here')
.digest('hex');

Java with Guava library

String sha256hex = Hashing.sha256()
.hashString('put_your_Xendit_secret_API_key_here', StandardCharsets.UTF_8)
.toString();

Java with Apache Commons Codecs

String sha256hex = DigestUtils.sha256Hex('put_your_Xendit_secret_API_key_here');
hash('sha256', 'put_your_Xendit_secret_API_key_here');

All the above should generate a hex string which will be your shared secret used to sign transactions, which will look like this

57425b47283422a8b0dd567374dd179232daca1da7f9cd21732b429d69b00f89

2. Generate a string representation of your transaction body

When generating the security signature, create a comma-separated “name:value” string of the POST fields that are included in the signed_field_names field. 

The ordering of the fields in the string is critical to the signature generation process. 

For a sample Safe Acceptance request like that below,

Note: you need to hash your authorization to the base64 format.

{
"amount": 10000,
"authorization": "Basic eG5kX3B1YmxpY19wcm9kdWN0aW9uX2NoQXBYaUVlVjVVbkdkZm9GUm1vdHk6",
"reference_id": "test-transaction",
"request_timestamp": "2020-01-01T00:00:00.000Z",
"return_url": "https://merchant.com/checkout/completed",
"signed_field_names": "amount,authorization,return_url,reference_id,signed_field_names,transaction_timestamp"
}

The string to be signed is amount=10000,authorization=Basic eG5kX3B1YmxpY19wcm9kdWN0aW9uX2NoQXBYaUVlVjVVbkdkZm9GUm1vdHk6,redirect_url=https://merchant.com/checkout/completed,reference_id=test-transaction,signed_field_names=amount,authorization,redirect_url,reference_id,signed_field_names,request_timestamp=2020-01-01T00:00:00.000Z

3. Sign the string with your shared secret

Signature creation should be created using HMAC with the SHA256 algorithm and the shared secret key generated above. The string to be signed should be in the exact same order as the string value of signed_field_names.

const signature = crypto.createHmac('sha256', shared_secret)
.update(string_to_sign)
.digest('hex');

Java with Guava library

String signature = Hashing.hmacSha256(shared_secret)
.newHasher()
.putString(string_to_sign, UTF_8)
.hash()
.toString();

Java with Apache Commons Codecs

HmacUtils hm256 = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, shared_secret);
String signature = hm256.hmacHex(string_to_sign);
hash_hmac('sha256', string_to_sign, shared_secret, false)

4. Add the signature and signed_fields to the form that will be POST-ed to the safe acceptance API

Testing your implementation

The following example assumes that the secret API key is xnd_production_vkeTQhp5itRjUrGresYdi0t0kkY.

Request signature generation

Given the following parameters:

{
"amount": 10000,
"authorization": "Basic eG5kX3B1YmxpY19wcm9kdWN0aW9uX2NoQXBYaUVlVjVVbkdkZm9GUm1vdHk6",
"reference_id": "test-transaction",
"request_timestamp": "1610678291403",
"redirect_url": "https://merchant.com/checkout/completed",
"signed_field_names": "amount,authorization,redirect_url,reference_id,signed_field_names,transaction_timestamp"
}

The result of your signature creation will look like this: 

847988a920b31da8c1f124a1930569b6444cf70abb34e8c22620d069ccc367fe

Response signature validation

You will validate the “signature” field using the shared secret that only you and Xendit will know.

Given the following parameters:

{
"created": "2019-07-15T15:54:52.141Z",
"business_id": "5d08a4nfea3b620019cfa213c",
"authorized_amount": 1200000,
"reference_id": "TVLK-123456",
"merchant_reference_code": "5d1ec8f4a3bcd10019a7e2de",
"masked_card_number": "400000XXXXXX0002",
"charge_type": "MULTI_USE_TOKEN",
"card_brand": "VISA",
"card_type": "CREDIT",
"status": "CAPTURED",
"bank_reconciliation_id": "5622988916826241203012",
"eci": "05",
"capture_amount": "1200000",
"currency": "IDR",
"id": "5d1eca0ca3bcd10019a7e2ee",
"authorized_amount": "1200000",
"merchant_id": "00080091009103589348501",
"mid_label": "xendit_ctv_agg",
"descriptor": "MERCHANT*EXPERIENCE",
"signed_field_names": "created,business_id,authorized_amount,reference_id,merchant_reference_code,masked_card_number,charge_type,card_brand,card_type,status,bank_reconciliation_id,eci,capture_amount,currency,id,authorized_amount,merchant_id,mid_label,descriptor",
"signature": "df212f41629f11d50128f2742963e103a52db30f4da9948b38318edfbf0ab470"
}

The result of your signature validation should check that the response body matches this signature:

df212f41629f11d50128f2742963e103a52db30f4da9948b38318edfbf0ab470

Note that you must also check if the timestamp (the ‘created’ field) of the Response is within a reasonable range from the timestamp where your system receives the Response.

If the time when your system received it is significantly later than the Response transaction timestamp, this may mean that a third party intercepted Xendit’s Response and sent you their own, which is a security risk.

Xendit recommends a range of <5 minutes difference between the Response timestamp and the actual time where your system receives it.

Was this page helpful?