Skip to content

MFA Transaction Signing

Enrol a user, set a PIN, and sign a transaction with a hardware-backed key.

enrollForMfa, setMfaPin and signAction are the Android core’s MFA flow. Read MFA Transaction Signing first for the three-call flow and what the token proves.

Every call has a blocking form and an …Async form taking a callback. The blocking forms do network I/O, so call them off the main thread.

Java:

// 1. Once per (user, device), after login. No biometric prompt appears here.
AlphynSDK.enrollForMfaAsync(userId, result -> {
if (!result.isSuccess()) {
Log.w("Byteria", "enrol failed: " + result.getErrorCode());
return;
}
Log.i("Byteria", "key is in " + result.getSecurityLevel());
});
// 2. Set or change the PIN. Verified and hashed ON THE SERVER.
char[] newPin = pinField.getText().toString().toCharArray();
AlphynSDK.setMfaPinAsync(userId, newPin, null, result -> { /* … */ });
// 3. Per transaction, after the user clears your confirmation screen.
String context = "{\"amount\":\"250.00\",\"currency\":\"TRY\",\"toIBAN\":\"TR12…\"}";
char[] pin = pinField.getText().toString().toCharArray();
ActionVerificationResult result =
AlphynSDK.signAction(userId, "TRANSFER", context, pin);
if (result.isSuccess()) {
sendToYourBackend(result.getVerificationToken());
}

AlphynSDK zeroes every char[] it is handed before returning. Build the array and pass it; do not keep a String copy of the PIN, because a String cannot be wiped.

// Enrolment
public static MfaResult enrollForMfa(String userId)
public static void enrollForMfaAsync(String userId, MfaCallback callback)
// PIN
public static MfaResult setMfaPin(String userId, char[] newPin, char[] oldPin)
public static void setMfaPinAsync(String userId, char[] newPin, char[] oldPin,
MfaCallback callback)
// Signing
public static ActionVerificationResult signAction(
String userId, String actionLabel, String contextJson, char[] pin)
public static ActionVerificationResult signAction(
String userId, String actionLabel, String contextJson, char[] pin,
AlphynSubject subject)
ParameterTypeNotes
userIdStringyour identifier for the logged-in user, not one Alphyn issues
newPin / oldPinchar[]zeroed by the call. oldPin is null on a first set
actionLabelStringyour own name for the operation, e.g. TRANSFER
contextJsonStringthe transaction as a JSON object. Its hash is bound into the signature
pinchar[]optional. null omits the factor rather than failing
subjectAlphynSubjectoptional. null is byte-identical to the four-argument overload
MethodMeaning
isSuccess()whether the operation succeeded
getErrorCode()the backend’s code verbatim, null on success
getErrorMessage()human-readable; do not branch on it
getSecurityLevel()enrolment only: where the key ended up, e.g. STRONGBOX
getEnrollmentId()enrolment only: the backend’s identifier for this enrolment

isSuccess(), getVerificationToken(), getExpiresAtMs(), getErrorCode(), getErrorMessage().

Read the token with getVerificationToken(). Never with toString(): that is a debug rendering and it truncates the token to its first twenty characters, which your backend can only reject.

AlphynSubject subject = AlphynSubject.builder()
.tc("10000000146")
.phone("+905342223344")
.build();
ActionVerificationResult result =
AlphynSDK.signAction(userId, "TRANSFER", context, pin, subject);

build() never throws and never fails, for any input including null. A field that does not conform is dropped, never corrected, and a subject in which nothing survived produces a request byte-identical to one that declared none. getTcHash(), getPhoneHash() and getEmailHash() return the tokens, or null for a field that was dropped; isEmpty() reports whether anything survived.

The raw values never leave the device. See Subject identity for the accepted forms and for why a token must be treated as identifying data.

  • Signing a transfer, withdrawal or beneficiary change so your backend can prove afterwards that a specific user’s device authorised those exact values
  • Adding a second factor to a high-risk action without an SMS round trip
  • Correlating a signed transaction with a person across institutions, through a subject token
void confirmTransfer(String userId, String iban, String amount, char[] pin) {
JSONObject ctx = new JSONObject();
ctx.put("amount", amount);
ctx.put("currency", "TRY");
ctx.put("toIBAN", iban);
AlphynSubject subject = AlphynSubject.builder().tc(customerTckn).build();
executor.execute(() -> {
ActionVerificationResult result = AlphynSDK.signAction(
userId, "TRANSFER", ctx.toString(), pin, subject);
if (!result.isSuccess()) {
// THREAT_DETECTED is a refusal, not a transport failure. Do not retry silently.
runOnUiThread(() -> showError(result.getErrorCode()));
return;
}
// Your backend validates this token before it moves anything.
yourApi.transfer(iban, amount, result.getVerificationToken());
});
}

The token is evidence, not a decision. Your backend must validate it server-side and compare the echoed context against the transfer it is about to perform; see Backend verification.