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.

1 Like

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 Rapyd Docs | Error

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.

Greetings,
Could you solve?

The same thing happens to me with post methods.

Error:

org.springframework.web.client.HttpClientErrorException$Unauthorized: 401 Unauthorized: [{“status”:{“error_code”:“UNAUTHORIZED_API_CALL”,“status”:“ERROR”,“message”:“”,“response_code”:“UNAUTHORIZED_API_CALL”,“operation_id”:“4db473a4-bec0-49ff-b2f2-a6ea4c919957”}}]

Thanks @jcanare2, have you checked out this resource? Java Payment Gateway Integration with Rapyd

What endpoint are you using? Typically unauthorized may be a wrong endpoint.

Are you working in Sandbox?

1 Like

I’m working on this endpoint:

https://sandboxapi.rapyd.net/v1/customers

Method: post

Yes, Sandbox:

My body String is:

{“business_vat_id":“123456789”,“email”:"johndoe@rapyd.net”,“ewallet”:“ewallet_****************”,“invoice_prefix”:“JD-”,“name”:“John Doe”}

Note: With get methods it works fine, but with post method don’t work

Error:

401 Unauthorized: [{“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 … (415 bytes)]

Is there a resource to validate signatures with logs?
Any idea?

@jcanare2

It must be the way you are parsing the body. It has to be a valid json presented as a String with no spaces.

My guess is that the replaceAll() function might be messing up the string, since you have some URLs.

Since you already have a working PHP script, you can generate and print the signature using that PHP script. Then hard code the time stamp and salt used by the PHP script into your Java example, and generate and print the signature with Java.

This way you can troubleshoot your Java code.

1 Like