Skip to content

MFA Transaction Signing

Enrol a user, set a PIN, and sign a transaction from JavaScript, on Android and iOS alike.

Read MFA Transaction Signing first for the three-call flow and what the token proves.

All three resolve rather than throw. A refusal arrives as { success: false, errorCode }, so the transaction path and the error path are the same await.

import { AlphynSDK } from '@alphyn/react-native-sdk';
// 1. Once per (user, device), after login. No biometric prompt appears here.
const enrolment = await AlphynSDK.enrollForMfa(userId);
if (!enrolment.success) {
console.warn('enrol failed:', enrolment.errorCode);
}
// 2. Set or change the PIN. Verified and hashed ON THE SERVER.
await AlphynSDK.setMfaPin(userId, newPin); // first time
await AlphynSDK.setMfaPin(userId, newPin, oldPin); // changing it
// 3. Per transaction, after the user clears your confirmation screen.
const result = await AlphynSDK.signAction(
userId,
'TRANSFER',
{ amount: '250.00', currency: 'TRY', toIBAN: 'TR12…' },
pin,
);
if (result.success) {
await sendToYourBackend(result.verificationToken);
}
AlphynSDK.enrollForMfa(userId: string): Promise<MfaResult>
AlphynSDK.setMfaPin(
userId: string, newPin: string, oldPin?: string | null
): Promise<MfaResult>
AlphynSDK.signAction(
userId: string,
actionLabel: string,
context: ActionContext,
pin?: string | null,
subject?: AlphynSubject | null,
): Promise<ActionVerificationResult>
ParameterTypeNotes
userIdstringyour identifier for the logged-in user. Empty resolves with ARG_ERROR
newPin / oldPinstringomit oldPin on a first set. An empty string is treated as omitted
actionLabelstringyour own name for the operation. Empty resolves with ARG_ERROR
contextActionContextthe transaction. Its hash is bound into the signature
pinstring | nulloptional. null omits the factor rather than failing
subjectAlphynSubject | nulloptional. See below

The PIN crosses as a string here because that is what JavaScript has; the native bridge converts it to bytes and zeroes its own copy. A JavaScript string cannot be wiped, so do not hold one longer than the call.

{ success: boolean; errorCode?: string; errorMessage?: string;
securityLevel?: string; enrollmentId?: string }

errorCode is the backend’s string verbatim. Branch on it, never on errorMessage.

{ success: boolean; verificationToken?: string; expiresAtMs?: number;
errorCode?: string; errorMessage?: string }
const result = await AlphynSDK.signAction(
userId,
'TRANSFER',
{ amount: '250.00', currency: 'TRY' },
pin,
{ tc: '10000000146', phone: '+905342223344' },
);

AlphynSubject is a plain object with three optional string fields, tc, phone and email. The values cross to the native core, which validates and tokenises them; nothing is hashed in JavaScript.

An object whose fields are all undefined reaches native as no identity, not as three empty strings; an empty string would be validated, dropped, and would read in a log like a caller who sent garbage rather than one who sent nothing.

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. 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
async function confirmTransfer(userId: string, iban: string, amount: string, pin: string) {
const result = await AlphynSDK.signAction(
userId,
'TRANSFER',
{ amount, currency: 'TRY', toIBAN: iban },
pin,
{ tc: customerTckn },
);
if (!result.success) {
// THREAT_DETECTED is a refusal, not a transport failure. Do not retry silently.
showError(result.errorCode);
return;
}
// Your backend validates this token before it moves anything.
await yourApi.transfer(iban, amount, result.verificationToken);
}

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.