Skip to content

Backend Verification

What your server does with the token signAction returns, and where it reads the subject.

The token the SDK hands your application is not a decision. It is evidence, and it is only worth anything once your backend has validated it server-to-server against the Alphyn backend. A client that reports “signed successfully” is a client, and a compromised one says the same thing.

The flow:

  1. your app calls signAction and receives verificationToken;
  2. your app sends that token to your backend along with the transaction it is about to perform;
  3. your backend calls Alphyn’s verification endpoint with its Backend verify key;
  4. your backend compares what Alphyn echoes back against the transaction it was asked to perform;
  5. only then does it move the money.

Steps 4 and 5 are the ones that matter. A backend that calls the endpoint and acts on valid: true alone has verified that a transaction was signed, not this one.

POST https://alphyn-attestation-api.byterialab.com/api/public/action/backend-verify
X-API-Key: <your Backend verify key>
Content-Type: application/json
{ "verificationToken": "<the token from signAction>" }

The Backend verify key is issued per registered app in the Alphyn dashboard and is not the API key your build registration uses. It belongs on your server and nowhere near your application bundle.

When the token is valid:

FieldWhat it is for
validtrue. Necessary, nowhere near sufficient
verificationIdunique identifier for this verification
deviceIdthe device that signed
appHashthe application hash, for app identity
packageNamethe application’s package name or bundle identifier
contextHashhash of the context that was signed
actionLabelthe label you passed, for example TRANSFER
contextthe context that was signed, echoed back
issuedAt / expiresAtthe token’s validity window

Check all four. Each of them is a real attack if you skip it.

1. contextHash matches the transaction you are about to perform. This is the whole point. Bind the token to the exact amount and destination in front of you, not merely to “a transfer”. Without this check a token signed for a 10 TRY transfer authorises a 100 000 TRY one.

2. expiresAt has not passed. Tokens are short-lived by design; treat a stale one as unsigned.

3. actionLabel is the operation you are executing. A token signed for LOGIN is not authorisation for WITHDRAW.

4. packageName and appHash are yours. They are what stops a token minted by another application from being spent on yours.

The subject tokens are not a separate field. They ride inside the transaction context, so they come back inside the echoed context, under a nested subject object:

{
"valid": true,
"actionLabel": "TRANSFER",
"context": {
"amount": "250.00",
"currency": "TRY",
"subject": {
"tc": "0247d10cbc2b05efa49fe02b15ad274d15031abf05a87fffcf461795ccbc6901",
"phone": "e47be99e43d11b27bd452dfe825bb979dacc4e9aec9ec7c92dc7547d6a250561"
}
}
}

Because they travel inside the context, they are covered by the same signature and by the same contextHash check as every other field. A subject that was altered in flight fails check 1 above, along with everything else in the context.

A field that was dropped as malformed is absent from that object rather than present and empty. Do not read an empty string as “declared but invalid”: from your backend’s side, dropped and never declared are the same thing, deliberately.

These tokens are identifying data. They are stable pseudonyms, not anonymised values, and the valid national-identifier space is small enough to enumerate. Whatever your policy is for storing a national identification number applies to its token. See Subject identity.

It does not tell you whether the user is allowed to make this transfer, whether the account has the balance, or whether your own fraud rules are satisfied. It tells you that a specific enrolled user’s device signed a specific context at a specific time, on a device whose state Alphyn had attested. Every business decision is still yours.