ISO 20022 User Interface Considerations: Legal Entity Identifier (LEI)

Capture, validate and lookup LEIs in a user interface.

Aim

In this article, using a (working) example user interface, we’ll be validating data entered by a user, confirming if it is a valid LEI.  If so, we’ll grab the details from GLEIF and present these back to the user in the UI, allowing them to confirm it is, or is not, the LEI for their intended creditor.  If the validation fails, we’ll display the reason.

Why? Typing an LEI has the potential for mistakes, allowing an invalid or incorrect LEI into the payment chain, causing friction, such as through screening queries and ultimately payment rejection. We’ll focus on solutions to minimise this.

Overview

In the ISO 20022 era, there may be a requirement for LEIs in a payment.  LEIs for the agents (banks) in the chain should be retrieved from standard settlement instructions or bank almanac / reference data, such as the SwiftRef data which now includes LEIs, not from a user input.  The same should apply to the debtor (bank’s customer), requiring the onboarding journey to capture LEIs.  Both these topics are out of this article’s scope but ought to be on banks’ ISO 20022 roadmaps.

What remains are LEIs which will likely come from user input, such as for the creditor (if this is not also in a persisted store of payees).

If a refresher on LEIs is needed, please visit the GLEIF website and as we’ll be discussing BIC/LEI mapping, see this related article.

See the video of the the demo user interface here:

The User Need

Let’s start with a user story.

“As a corporate banking customer, I want to be able to insert the correct LEI of my supplier (creditor) into a payment request via my banking application, so that the identity of the creditor is clear and allows frictionless processing of the payment.”

As a…In this case the user is a corporate banking customer, but this could apply to any user type who is initiating the payment.  LEIs only apply to organisations (not all will have one) so making a payment to a creditor who is an individual would not apply.

I want…inserting an LEI may not always be required but when it is, the process to capture it must be simple and transparent. If entered incorrectly, it must allow corrections.

So that…currently the requirement for using LEIs in payments is limited to specific use cases, such as for agents in some high value payment schemes. It is one of several richer data elements intended to reduce friction in any leg of the payment journey.

In summary, whilst the use of an LEI may be optional, if used, it needs to be valid, and details presented back to the user so they can assure themselves it is correct.

The Solution

Our demonstration user interface will allow the user to:

  1. Capture an LEI as user input and
  2. Lookup an LEI and choose from the matched organisation(s)

In both cases it will validate the LEI and return related information to the UI to allow the user to determine if it is correct.

Our UI will be simple, see Figure 1:

  • a text field to capture the user input LEI.
  • a hyperlink to a pop-up window for the LEI lookup.
  • a text area to display the results of the LEI validation or the details of the LEI if it passes validation.

Figure 1.  Demo LEI user interface

1. Capture an LEI

The UI will have a single text field allowing the user to enter a LEI (or value they believe to be an LEI).  When the focus leaves the text field, the user input will be validated, and the UI displays the results of the validation.  If it is a valid LEI, the system will retrieve the detailed record of the LEI (via GLEI API) and display a summary in the UI, allowing the user to verify it is for their intended creditor.  In principle, this could be extended to match the LEI name against the name of the creditor which will have been entered.

LEI Validation

The data entered will be validated in separate steps, providing a more precise reason for any failure back to the user.  A simple “valid” or “invalid” response is a poor user experience and is difficult for them to understand what needs correcting. We offer an API to validate an LEI, read the document ation here: https://api.affinis.co.uk/docs.

  • Check LEI length is 20 characters.  A simple string length check.  Figure 2 shows the result of failing this check with the reason for the failure presented to the user.

Figure 2.  Validate length

  • Check LEI pattern is alphanumeric (uppercase).  Check the value against this regex ^[A-Z0-9]*$ Figure 3 shows the result of failing this check.

Figure 3.  Validate uppercase alphanumeric

  • Check LEI checksum.  The format of an LEI is 20 alphanumeric characters including a checksum (characters 19 and 20).  As with any identifier which incorporates a checksum (such as IBANs), the checksum exists for a reason, specifically for verification.  So always verify it.  Validating only that the entered data is 20 alphanumeric characters is inadequate.  Refer to Appendix A.  Code – Validate LEI, for the checksum verification. Figure 4 shows the result of failing this check.

Figure 4.  Validate checksum

Valid LEI Record Retrieval

If the validation checks are all successful, the user will be presented with confirmation and the details of the organisation for this LEI.  It is important to note:

  • An Entity (organisation) can have multiple LEIs.
  • Each registered LEI has a status, such as PENDING_VALIDATION, ISSUED, LAPSED, DUPLICATE, RETIRED, ANNULLED, CANCELLED, TRANSFERRED, PENDING_TRANSFER, PENDING_ARCHIVAL (and MERGED which is deprecated).   

Figure 5 shows the summary of the valid LEI presented back to the user including:

  • Line 1.  Entity (organisation) legal name (other names are held in the LEI record if they exist)
  • Line 2.  Entity legal address (the LEI record holds multiple addresses such as registered and headquarters addresses)
  • Line 3.  LEI registration status (there is a different status for the entity itself)
  • Line 4.  Swift BICs (all Swift BICs that are mapped to this LEI – note that LEI to BIC relationship is many-to-many)

Figure 5.  Valid LEI showing organisation details

This LEI data is retrieved by calling the GLEIF API endpoint lei-records, passing the LEI, then parsing out these data elements from the response.

https://api.gleif.org/api/v1/lei-records/2W8N8UU78PMDQKZENC08

See Appendix B.  Code – Parse LEI Details, for a snippet of the code to parse out the response from the API request.

2. Lookup an LEI

It may be that when the LEI details are presented back to the user, they realise it is wrong.  In B2B payments, a corporate user may be familiar with the BIC of the creditor and wants to use this to find the LEI; ; a lookup capability would be helpful.

Our LEI Lookup screen will demonstrate this capability.  It will:

  • Accept a user input for the search parameter.  This can be either a BIC or the name of the organisation.
  • Use the GLEIF APIs to retrieve any LEI records which match.
  • Display the matched records in a table, allowing the user to select the correct one.

Figure 6 shows this LEI lookup dialog.

Figure 6.  LEI lookup dialog

Whilst there are options for which GLEIF API endpoint to use, our lookup uses only lei-records endpoint regardless of the value entered.  On clicking the Search button, the value is parsed and:

  1. If it is a valid BIC format, the GLEIF API endpoint lei-records is called with the BIC as the filter.
  2. If it is not a valid BIC format, it is assumed to be a full or partial name, and the same lei-records endpoint is used but with the legal name as the filter.

Both API calls may return multiple records, and the code should therefore handle the pagination which GLEIF supports.  There are rate limits on the API calls and the number of records per page.

When searching based on the legal name, GLEIF also offers the fuzzycompletions endpoint, which matches based on fuzzy logic, that is statistical similarity.  We’ve chosen not to use this endpoint as the lei-records will match against “contains” and is more predictable than fuzzy logic.

A fuzzycompletions call on the legal name field would be:

https://api.gleif.org/api/v1/fuzzycompletions?field=entity.legalName&q=INTESA

and a similar call but using full text search would be:

https://api.gleif.org/api/v1/fuzzycompletions?field=fulltext&q=INTESA

We do also offer APIs for LEI lookups, including using a BIC parameter:

https://api.affinis.co.uk/v1/identity/leis/INGCITM1XXX/lei

1. GLEIF API Endpoint lei-records – BIC

The user input BIC is passed in this API call as follows:

https://api.gleif.org/api/v1/lei-records?filter[bic]=INGCITM1XXX&page[number]=1&page[size]=100

A filter is applied for the defined field, BIC, in this case.  Full details of this endpoint and the fields available for filtering can be found on the GLEIF website.

The data returned is an array of records which we then parse out the summary elements to load into the dialog table.  Please contact us for the code used to parse out the LEI details or any other parts of the demo.

Note:  This GLEIF endpoint for the BIC filter expects a BIC11 not BIC8.  If you only have the BIC8, add XXX to the end or ensure the code does this before the API request.

Figure 7 shows the results when searching for the BIC INGCITM1XXX.  The user can select the correct LEI, if more than one is returned.

Figure 7.  LEI lookup for BIC

2. GLEIF API Endpoint lei-records – Legal Name

If the user input is not a BIC (see Appendix C.  Code – Validate BIC), it is treated as a full or partial name and the data is passed in this API call:

https://api.gleif.org/api/v1/lei-records?filter[entity.legalName]=INTESA&page[number]=1&page[size]=100

A filter is applied for the defined field, entity.legalName, in this case. 

As with the previous BIC call, the data returned is an array of records and we can use the pagination features to iterate through the returned pages and parse out the summary elements to load into the dialog table.

Note:  This GLEIF endpoint for the entity.legalName filter will match where the legal name contains the value entered – it does not need to be an exact match.

Figure 8 shows the 85 results loaded into the UI table when searching for the name “INTESA”.  The user can select the correct LEI from the list.

Figure 8.  LEI records returned in legal name search

Summary

High-level cases studies and white papers exhorting the benefits of LEIs are plentiful.  Detailed papers on the practicalities of implementing LEIs, in back-office systems or as we have focused on here, in front-end user interfaces, are less common.  We’ve covered business and technical aspects here, both limited in scope but hopefully sufficient to offer options for your own implementations.

To recap, the use of LEIs will grow and where they relate to persisted customer or bank data, these data stores need to be updated with this information, ready for use in payments when necessary.  Ensure your ISO 20022 roadmap includes this aspect.

Where LEIs relate to non-persisted data, often for creditors, there will be a need to capture and validate these in a UI and for additional customer satisfaction, provide a lookup capability using name, BIC or other parameters available through the GLEIF API.

Appendix A.  Code – Validate LEI

The code for this validation follows (replace each internationalisation (I18N) reference with the relevant string for the failure reason string):

public static class ValidationResult {
        public boolean isValid;
        public String reason;

        ValidationResult(boolean isValid, String reason) {
            this.isValid = isValid;
            this.reason = reason;
        }
    }

private static ValidationResult isValidLei(String lei) {
        // LEI is not null
        if (lei == null) {
            return new ValidationResult(false, "LEI is null");
        }
        
        // Is a 20-character alphanumeric string
        if (lei.length() != 20) {
            return new ValidationResult(false, I18N.get("leivalidation.invalid.length"));
        }

        // Check if its alphanumeric (upper-case)
        if (!lei.matches("^[A-Z0-9]*$")) {
            return new ValidationResult(false, I18N.get("leivalidation.invalid.alphanumericcase"));
        }

        // Checksum validation
        // 1. Take the first 18 chars of the 20 chars string
        String leiToCheck = lei.substring(0, 18);

        // 2. Take the last 2 chars of the 20 chars
        String originalChecksum = lei.substring(18);

        // 3. Convert all letters to number equivalent and append two zeros
        StringBuilder convertedLei = new StringBuilder();
        for (char c : leiToCheck.toCharArray()) {
            if (Character.isLetter(c)) {
                convertedLei.append(c - 'A' + 10);
            } else {
                convertedLei.append(c);
            }
        }
        convertedLei.append("00");

        // 4. Concatenate all numbers and perform MOD 97
        BigInteger number = new BigInteger(convertedLei.toString());
        BigInteger remainder = number.mod(BigInteger.valueOf(97));

        // 5. Calculate the checksum
        int calculatedChecksum = 98 - remainder.intValue();

        // 6. Compare calculated checksum to the original checksum
        String calculatedChecksumStr = String.format("%02d", calculatedChecksum);
        if (!calculatedChecksumStr.equals(originalChecksum)) {
        	return new ValidationResult(false, I18N.get("leivalidation.invalid.checksum"));
        }
        
        return new ValidationResult(true, I18N.get("leivalidation.valid.text"));
    }

Appendix B.  Code – Parse LEI Details

Takes the json data response from the GLEIF lei-records call and parses out the 5 LEI fields used in the UI.

/***
	 * Parse out the summary fields of the response.body (jsonData) 
	 * which is returned from the GLEIF lei-records endpoint
	 * 
	 * This calls buildLegalAddress() to parse out the nested Legal Address into a single variable
	 * 
	 * LEISummary is a class of the 4 string variables to hold the selected LEI data elements
	 * - LEI number 
	 * - Legal Name
	 * - Registration status
	 * - Legal Address (concatenated)
	 * - BIC(s)

	 * @param jsonBody  The response.body from the API request
	 * @return List<LEISummary>
	 */
	private static List<LEISummary> parseLEIBICDetails(String jsonBody) {
		List<LEISummary> leiSummaries = new ArrayList<>();
		try {
			ObjectMapper mapper = new ObjectMapper();

			// Parse the JSON string into a Map
			Map<String, Object> jsonData = mapper.readValue(jsonBody, Map.class);

			// Access the "data" array
			List<Map<String, Object>> data = (List<Map<String, Object>>) jsonData.get("data");

			// Loop through each LEI record
			for (Map<String, Object> record : data) {
				LEISummary summary = new LEISummary();
				Map<String, Object> attributes = (Map<String, Object>) record.get("attributes");
				Map<String, Object> entity = (Map<String, Object>) attributes.get("entity");
				JsonNode jsonLegalAddress = mapper.valueToTree(entity.get("legalAddress"));
				Map<String, Object> legalName = (Map<String, Object>) entity.get("legalName");
				Map<String, Object> registration = (Map<String, Object>) attributes.get("registration");

				summary.setLei((String) attributes.get("lei"));
				summary.setLegalName((String) legalName.get("name"));
				summary.setStatus((String) registration.get("status"));
				summary.setSwiftBic(attributes.get("bic") != null ? attributes.get("bic").toString() : null);
				summary.setLegalAddress(buildLegalAddress(jsonLegalAddress));
				leiSummaries.add(summary);
			}
		} catch (JsonProcessingException e) {
			e.printStackTrace();
		}

		return leiSummaries;
	}

Appendix C.  Code – Validate BIC

The code to check if a string is a valid BIC.

/**
	 * Validates if a string is a valid BIC (AnyBIC)
	 * @param bic  The string to validate
	 * @return true if the string is a valid BIC, else false
	 */
	public static boolean isValidBIC(String bic) {
		// Regular expression for BIC validation
		Pattern bicPattern = Pattern.compile("^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$");

		if (bic == null) {
			return false;
		}

		return bicPattern.matcher(bic).matches();
	}

Leave a Reply

Your email address will not be published. Required fields are marked *