Running into error while demoing Rapyd on Java

Hi guys.

I’m demoing Rapyd’s payment Gateway API for Java. When I run my demo webapp, I receive an error that reads:

{“status”:{“error_code”:“UNAUTHENTICATED_API_CALL”,“status”:“ERROR”,“message”:“The API received a request, but the signature did not match. The request was rejected. Corrective action: (1) Remove all whitespace that is not inside a string. (2) Remove trailing zeroes and decimal points, or wrap numbers in a string.”,“response_code”:“UNAUTHENTICATED_API_CALL”,“operation_id”:“cb511db8-4a6f-4627-9dac-f9c4790a390f”}}|#]

My code is as follows:

public class CheckoutServlet extends HttpServlet{
    String cancelCheckoutURL = "https://example.com/cancel";
    String completeCheckoutURL = "https://example.com/complete";
    String country = "US";
    String currency = "USD";
    String language = "en";
    
    
    public static String hash256(String data) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        md.update(data.getBytes());
        return bytesToHex(md.digest());
    }
    
    public static String bytesToHex(byte[]bytes) {
        StringBuffer result = new StringBuffer();
        for (byte byt: bytes)
            result.append(Integer.toString((byt & 0xff) + 0x100, 16).substring(1));
        return result.toString();
    }
    
    public static String hmacDigest(String msg, String keyString, String algo) {
        String digest = null;
        try {
            SecretKeySpec key = new SecretKeySpec((keyString).getBytes("ASCII"), algo);
            Mac mac = Mac.getInstance(algo);
            mac.init(key);
            
            byte[]bytes = mac.doFinal(msg.getBytes("UTF-8"));
            
            StringBuffer hash = new StringBuffer();
            for (int i = 0; i < bytes.length; i++) {
                String hex = Integer.toHexString(0xFF & bytes[i]);
                if (hex.length() == 1) {
                    hash.append('0');
                }
                hash.append(hex);
            }
            digest = hash.toString();
        } catch (UnsupportedEncodingException e) {
            System.out.println("hmacDigest UnsupportedEncodingException");
        }
        catch (InvalidKeyException e) {
            System.out.println("hmacDigest InvalidKeyException");
        }
        catch (NoSuchAlgorithmException e) {
            System.out.println("hmacDigest NoSuchAlgorithmException");
        }
        return digest;
    }
    
    public static String generateString() {
        int leftLimit = 97;   // letter 'a'
        int rightLimit = 122; // letter 'z'
        int targetStringLength = 10;
        Random random = new Random();
        StringBuilder buffer = new StringBuilder(targetStringLength);
        for (int i = 0; i < targetStringLength; i++) {
            int randomLimitedInt = leftLimit + (int)
                (random.nextFloat() * (rightLimit - leftLimit + 1));
            buffer.append((char)randomLimitedInt);
        }
        String generatedString = buffer.toString();
        
        return (generatedString);
    }
  @Override
  public void doPost(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
        try {
            System.clearProperty("javax.net.ssl.trustStore");
            System.out.println("GetPOS Start");
            String httpMethod = "post";                           // get|put|post|delete - must be lowercase
            String urlPath = "/v1/checkout"; 
            String basePath = "https://sandboxapi.rapyd.net"; // hardkeyed for this example
            String salt = generateString(); // Randomly generated for each request.
           
            long timestamp = System.currentTimeMillis() / 1000L; // Unix time (seconds).
            String accessKey = "0F32811C67FADC15E0ED";                    // The access key received from Rapyd.
            String secretKey = "d8abc747ebfec6cb10a049a9904d7c7652cefeb4cafa6304b15a0176ccf4ef19eac4c0ba61ed8f65";                    // Never transmit the secret key by itself.
            String bodyString = new String("{"
                + "\"amount\": 10,\n" +
                "\"complete_checkout_url\":\"" + completeCheckoutURL +"\",\n" +
                "\"country\":\"" + country + "\",\n" +
                "\"currency\":\"" + currency + "\",\n" +
                "\"cancel_checkout_url\":\"" + cancelCheckoutURL + "\",\n" +
                "\"language\":\"" + language +  "\"}").replaceAll("\\s", "");                                     // Always empty for GET; strip nonfunctional whitespace.
                                                                      
            String toEnc = httpMethod + urlPath + salt + Long.toString(timestamp) + accessKey + secretKey + bodyString;
            System.out.println("String to be encrypted::" + toEnc);
            String StrhashCode = hmacDigest(toEnc, secretKey, "HmacSHA256");
            String signature = Base64.getEncoder().encodeToString(StrhashCode.getBytes());
            HttpClient httpclient = HttpClients.createDefault();
            
            try {
                HttpPost httppost = new HttpPost (basePath + urlPath);
                String json = new String("{access_key:" + accessKey + ",salt:" + salt + ",timestamp:" + Long.toString(timestamp) + ",signature:" + signature + "}").replaceAll("\\s", "");
                
                StringEntity entity = new StringEntity(json);
                httppost.setEntity(entity);
                httppost.setHeader("Accept", "application/json");
                httppost.setHeader("Content-type", "application/json");
                /*httppost.addHeader("Content-Type", "application/json");
                httppost.addHeader("access_key", accessKey);
                httppost.addHeader("salt", salt);
                httppost.addHeader("timestamp", Long.toString(timestamp));
                httppost.addHeader("signature", signature);*/
               // httppost.addHeader("idempotency", idempotency);
                
                // Create a custom response handler
                ResponseHandler < String > responseHandler = new ResponseHandler < String > () {
                     @ Override
                    public String handleResponse(
                        final HttpResponse response)throws ClientProtocolException,
                    IOException {
                        int status = response.getStatusLine().getStatusCode();
                        HttpEntity entity = response.getEntity();
                        
                        return entity != null ? EntityUtils.toString(entity) : null;
                    }
                };
                
                String responseBody = httpclient.execute(httppost, responseHandler);
                System.out.println("----------------------------------------Response");
                System.out.println(responseBody);
            } catch (Exception e) {
                System.out.println("URL:" + basePath + urlPath);
                System.out.println(e.getMessage());
                    
                for (StackTraceElement exc : e.getStackTrace()) {
                    System.out.println(exc.toString());
                }
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
                    
                for (StackTraceElement exc : e.getStackTrace()) {
                    System.out.println(exc.toString());
                }
        }
    }
 }

What am I doing wrong?

Thanks in advance.

Thanks @Main_Method, here are some tips regarding the error:

The error response mentions two things:

  1. All spaces and other whitespace outside of strings must be removed.
  2. Numbers should be sent in strings, not as integers/numbers with decimal points.

Here are some resources that may help:

I would also take a look around your body string, and and how it may not be parsed correctly.

Hi,

Thanks for the response. However, I’m at my wits end. I tried eliminating all white spaces using replace and I ensured that all numbers were being sent as string. I wrapped the amount in quotation marks in my body. That didn’t work.

I managed to get it to work on PHP but not Java. I really don’t know what I’m missing here. Is there anyway to see the signature being compared at the Webhook/API? It would help me debug and figure out where the mistake is occurring.

It could be how I’m generating the signature?

My last resort will be to embed a PHP servlet into my code. And I don’t want to do that.

Are there any other Java code samples or tutorials I can use?

I think you guys also need to fix the Java example on this page:
https://docs.rapyd.net/build-with-rapyd/reference/authentication

The URL path is all wrong.

I managed to use the example to complete a successful Get request from Rapyd. It’s the post I’m having trouble with now.

Thanks @Main_Method,

The urlPath is defined above the page you linked, so it could include any extension based on the request.

  • url_path is the portion of the URL after the base URI, such as https://api.rapyd.net. The URL path starts with /v1.

Sandbox would be https://sandboxapi.rapyd.net.

Have you been able to take a look at the resources I posted? About three of them relate to the issue I believe you are having, matching with the Webhook signature.

If your problem persist please reach out to Support as shown in Submitting a Support Ticket

We should have a Java integration tutorial coming soon!

Hi, I just saw your reply. I think I meant that this part of the code may be wrong:

HttpGet httpget = new HttpGet(“/v1/data/countries”);

This is from the Java Example of The Authentication page.

But now I see what you’re saying - that you’re meant to edit and fill in the blanks. Okay. I scanned through those resources. Maybe I missed something. I’ll look read through them again.