Card Embossed Name Validation
Overview
ConnectPay automatically checks the embossed name on every Create Card Application against the cardholder’s verified name and calculates one similarity score for the full name. Following description is focused on Consumer program score of 0.80. At 0.80 or higher the name is approved automatically. Below 0.80, the name goes to manual review if it meets the minimum requirement, and is rejected if it does not.
Validation flow
- You submit Create Card Application with
embossing.firstNameandembossing.lastName. - ConnectPay compares the full embossed name (first name and last name together) with the cardholder’s full name and calculates one Jaro-Winkler score.
- Score 0.80 or higher: auto approved, card creation continues.
- Score below 0.80, minimum requirement met: the application is held for manual review.
- Score below 0.80, minimum requirement not met: the application is rejected.
Name requirements
| Rule | Value |
|---|---|
| Minimum requirement | One full first name and one full last name of the cardholder |
| Can be left out | Middle names, extra given names, extra parts of a compound surname |
| Does not meet the minimum | Initials instead of the first name, shortened or truncated names, nicknames, last name as an initial, first and last name swapped |
| Field limits | firstName 1–12 characters, lastName 1–13 characters |
| Ignored in the comparison | Letter case, diacritics, hyphens, apostrophes, dots |
If a full name is longer than the field limit, use another given name or surname part that fits. If the name or surname exceeds the allowed symbol count, truncate from the end until it fits.
Examples
| Cardholder name | Embossed name | Score | Minimum met | Result |
|---|---|---|---|---|
| Jonas Petraitis | JONAS PETRAITIS | 1.00 | Yes | Auto approved |
| Živilė Kazlauskienė | Zivile Kazlauskiene | 1.00 | Yes | Auto approved |
| Łukasz Sørensen | Lukasz Sorensen | 1.00 | Yes | Auto approved |
| Jonas Petraitis | Jonas Petraitys | 0.97 | Yes | Auto approved |
| Anna Maria Svensson | Anna M Svensson | 0.96 | Yes | Auto approved |
| Anna Maria Svensson | Anna Svensson | 0.94 | Yes | Auto approved |
| Jonas Petraitis | Jon Petraitis | 0.92 | No | Auto approved |
| Anna Maria García López | Anna Lopez | 0.74 | Yes | Manual review |
| Anna Maria García López | Maria Garcia | 0.73 | Yes | Manual review |
| Jonas Petraitis | J. Petraitis | 0.78 | No | Rejected |
| Robert Jones | Bob Jones | 0.73 | No | Rejected |
| Jonas Petraitis | Petraitis Jonas | 0.73 | No | Rejected |
The score compares the full embossed name with the cardholder’s full name, so leaving out a middle name or a surname part lowers it. The minimum requirement only decides the outcome when the score is below 0.80.
Recommendation: check the name before you submit
Run the same check in your own system so applications pass automatically and avoid manual review delays.
- Pre-fill the embossed name from the cardholder’s verified first name and last name.
- If the cardholder edits it, run the check and warn them when the score is below 0.80.
- Ask the cardholder to correct the name before you submit Create Card Application.
Always compare against the name ConnectPay holds for the cardholder, not a name from another source.
Business white label program differences
To be Updated.
Implementing the check in your system
The check has three steps: normalise both names, build the accepted forms of the cardholder’s name, and check that the embossed value holds a full name that scores at least 0.80 with Jaro-Winkler.
Step 1: Normalise both names
Apply the same normalisation to the embossed value and to the cardholder’s name:
- Map letters that do not decompose, such as Ø → O, Ł → L, ß → SS.
- Remove diacritics (Unicode NFKD, then drop combining marks): Ž → Z, ė → e.
- Convert to upper case.
- Replace the separators
- ' . / , &with a space. - Remove any remaining character that is not A–Z or a space, then collapse repeated spaces.
Step 2: Build the accepted forms of the cardholder’s name
After normalising, split the cardholder’s first name and last name into words. Each word is a full name the embossed value may use, which covers left-out middle names and compound surnames. Do not add initials or truncated forms to this list.
Step 3: Score the embossed value
- Split the embossed value into words. Single letters are initials; longer words are name words.
- If the value has no name word, only initials, it fails. Initials are accepted only in the first name, after a full name word, and must match the first letter of one of the cardholder’s names.
- Score each name word with Jaro-Winkler against the cardholder’s words. A word more than one character shorter than the name it is compared with counts as a shortened name and scores 0.
- The part’s score is the lowest score among its name words. The name passes when the first name score and the last name score are both 0.80 or higher.
Use the standard Jaro-Winkler parameters: prefix scale 0.1 and a common prefix of up to 4 characters. Libraries such as Apache Commons Text (JaroWinklerSimilarity), RapidFuzz and jellyfish use these defaults. Check that your library returns a similarity (1 = identical) and not a distance (0 = identical).
Reference implementation (Java)
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public final class EmbossedNameCheck {
public static final double THRESHOLD = 0.80;
public enum Outcome { AUTO_APPROVED, MANUAL_REVIEW, REJECTED }
public record Result(double score, boolean minimumMet, Outcome outcome) { }
// Letters that Unicode decomposition does not reduce to A-Z
private static final Map<Character, String> EXTRA = Map.ofEntries(
Map.entry('Ø', "O"), Map.entry('ø', "o"), Map.entry('Ł', "L"), Map.entry('ł', "l"),
Map.entry('Đ', "D"), Map.entry('đ', "d"), Map.entry('ß', "SS"),
Map.entry('Æ', "AE"), Map.entry('æ', "ae"), Map.entry('Œ', "OE"), Map.entry('œ', "oe"));
private EmbossedNameCheck() { }
/** Full check: one score for the whole name, then the minimum requirement below the threshold. */
public static Result check(String embFirst, String embLast, String holderFirst, String holderLast) {
double score = jaroWinkler(normalize(embFirst + " " + embLast),
normalize(holderFirst + " " + holderLast));
boolean minimumMet = scorePart(embFirst, holderFirst, true) >= THRESHOLD
&& scorePart(embLast, holderLast, false) >= THRESHOLD;
Outcome outcome = score >= THRESHOLD ? Outcome.AUTO_APPROVED
: minimumMet ? Outcome.MANUAL_REVIEW
: Outcome.REJECTED;
return new Result(Math.round(score * 100) / 100.0, minimumMet, outcome);
}
public static double jaro(String s1, String s2) {
if (s1.equals(s2)) return 1.0;
int l1 = s1.length(), l2 = s2.length();
if (l1 == 0 || l2 == 0) return 0.0;
int window = Math.max(Math.max(l1, l2) / 2 - 1, 0);
boolean[] m1 = new boolean[l1], m2 = new boolean[l2];
int matches = 0;
for (int i = 0; i < l1; i++) {
for (int j = Math.max(0, i - window); j < Math.min(i + window + 1, l2); j++) {
if (!m2[j] && s1.charAt(i) == s2.charAt(j)) {
m1[i] = m2[j] = true;
matches++;
break;
}
}
}
if (matches == 0) return 0.0;
int t = 0, k = 0;
for (int i = 0; i < l1; i++) {
if (m1[i]) {
while (!m2[k]) k++;
if (s1.charAt(i) != s2.charAt(k)) t++;
k++;
}
}
t /= 2;
return ((double) matches / l1 + (double) matches / l2 + (double) (matches - t) / matches) / 3.0;
}
public static double jaroWinkler(String s1, String s2) {
double j = jaro(s1, s2);
int prefix = 0;
int max = Math.min(Math.min(s1.length(), s2.length()), 4);
while (prefix < max && s1.charAt(prefix) == s2.charAt(prefix)) prefix++;
return j + prefix * 0.1 * (1 - j);
}
/** Uppercase, strip diacritics, keep letters A-Z and single spaces only. */
public static String normalize(String name) {
StringBuilder sb = new StringBuilder();
for (char c : name.toCharArray()) sb.append(EXTRA.getOrDefault(c, String.valueOf(c)));
return Normalizer.normalize(sb.toString(), Normalizer.Form.NFKD)
.replaceAll("\\p{M}", "")
.toUpperCase(Locale.ROOT)
.replaceAll("[-'./,&]", " ")
.replaceAll("[^A-Z ]", "")
.replaceAll("\\s+", " ")
.trim();
}
/** Best score of one embossed word; a word more than one character shorter than the name it matches scores 0. */
private static double fullWordScore(String word, List<String> holderWords) {
double best = 0.0;
for (String h : holderWords) {
if (word.length() >= h.length() - 1) best = Math.max(best, jaroWinkler(word, h));
}
return best;
}
/** Minimum-requirement score of one field: 0 if it holds no full name. */
private static double scorePart(String embossed, String holderPart, boolean allowExtraInitials) {
List<String> embWords = words(normalize(embossed));
List<String> holderWords = words(normalize(holderPart));
if (embWords.isEmpty() || holderWords.isEmpty()) return 0.0;
List<String> full = new ArrayList<>(), initials = new ArrayList<>();
for (String w : embWords) (w.length() > 1 ? full : initials).add(w);
if (full.isEmpty()) return 0.0; // an initial alone never meets the minimum
if (!initials.isEmpty()) {
if (!allowExtraInitials) return 0.0;
for (String i : initials) { // an extra initial must belong to the holder's names
if (holderWords.stream().noneMatch(h -> h.charAt(0) == i.charAt(0))) return 0.0;
}
}
double min = 1.0;
for (String w : full) min = Math.min(min, fullWordScore(w, holderWords));
return min;
}
private static List<String> words(String s) {
return s.isEmpty() ? List.of() : Arrays.asList(s.split(" "));
}
}Example calls:
EmbossedNameCheck.check("Anna M", "Svensson", "Anna Maria", "Svensson");
// Result[score=0.96, minimumMet=true, outcome=AUTO_APPROVED]
EmbossedNameCheck.check("Maria", "Garcia", "Anna Maria", "García López");
// Result[score=0.73, minimumMet=true, outcome=MANUAL_REVIEW]
EmbossedNameCheck.check("J.", "Petraitis", "Jonas", "Petraitis");
// Result[score=0.78, minimumMet=false, outcome=REJECTED]Requires Java 16 or later (records). No external libraries.
Test your implementation
Run your code against the cases in the Examples table: every Automatic pass row must pass your check and every Manual review row must fail it. Also check the textbook values for plain Jaro-Winkler: MARTHA vs MARHTA = 0.961 and DIXON vs DICKSONX = 0.813.
A name that passes your check should pass ConnectPay’s automatic check. A name that fails is not rejected, but it goes to manual review and delays the card.
FAQ
Can the cardholder use a nickname or short form on the card? It will not pass the automatic check, so Mike for Michael or Jon for Jonas goes to manual review. Spelling variants of the full name, such as a transliteration, pass automatically if they score 0.80 or higher.
Does letter case or punctuation matter? No. Names are compared in upper case without diacritics, and hyphens, apostrophes and dots are treated as spaces.
What if the cardholder’s legal name is longer than the field limits? Use another full given name or surname part that fits within 12 characters for the first name and 13 for the last name. Truncated names go to manual review.
Is the company name on Visa Business cards validated the same way? No, this check covers the cardholder’s first name and last name only.
Can we lower the threshold for our program? No. The 80% threshold applies to all programs.