Skip to content

Subject Identity

Declare whose transaction you are signing, without the identifier ever leaving the device.

A subject is three optional fields (a Turkish national identification number, a phone number, an e-mail address) attached to a signAction call. What reaches the wire is never the value you supplied. Each field that conforms is replaced, inside the SDK, by

SHA-256("<kind>:" + value)

rendered as 64 lower-case hexadecimal characters, with kind being tc, phone or email. The raw value never leaves the device.

The point of the tokens is matching: two institutions that see the same person derive the same token from the same identifier, so a signed transaction can be correlated across them without either one publishing an identifier.

A token is a pseudonym, not an anonymised value

Section titled “A token is a pseudonym, not an anonymised value”

Read this before you decide where the tokens may be stored.

The hash is not a privacy control. There is deliberately no key, no HMAC, no salt and no iteration count, because matching requires every institution to derive the identical token from the identical identifier, and any secret would have to be the same everywhere and shipped inside an SDK on millions of devices, where it is extractable.

The consequence is direct: the space of valid national identification numbers is small enough to enumerate, so a token can be reversed by exhaustive search. Treat a subject token as identifying data everywhere it is stored, logged, exported or shared. It is a stable identifier that follows one person across institutions. It is not anonymous, and nothing about it being a hash makes it safe to print.

Declaring an identity can never cost a transaction

Section titled “Declaring an identity can never cost a transaction”

This is a guarantee, not a best effort:

  • a field that does not conform is dropped, and the rest of the call proceeds;
  • a subject in which nothing survived produces a request byte-identical to one that declared no subject at all;
  • an SDK too old to carry an identity signs without one.

There is no input, including none at all, for which building a subject fails or throws. A malformed identity must never be able to break a payment.

A value that does not conform is discarded. It is not repaired, reformatted or normalised.

You supplyWhat happens
"0534 222 33 44"dropped. It is not reformatted into +905342223344
"[email protected]"dropped. It is not lower-cased into [email protected]
" 10000000146"dropped. Padding is not trimmed

This is deliberate and it is worth understanding, because the alternative looks friendlier and is worse. Correcting a value would mean the SDK reports an identifier the integrator never declared, and every institution would then have to agree on the identical correction for any match to hold. A wrong value costs a match. A silently corrected one can match the wrong person.

tc: Turkish national identification number

Section titled “tc: Turkish national identification number”
  • exactly 11 ASCII digits, 0–9 only;
  • the first digit is not 0;
  • the TCKN checksum holds.

Other scripts’ decimal digits are not accepted, even where a Unicode digit test would accept them.

  • a leading +, then 8 to 15 ASCII digits and nothing else.

No spaces, dashes, parentheses or leading zeros are stripped, because nothing is stripped. The bounds are load-bearing: unbounded, both "+9" and a twenty-digit string would conform, and two integrators shipping the same placeholder would have their users collapse into one person.

  • entirely lower-case. An address containing any of A–Z is dropped, before the grammar is even considered;
  • exactly one @;
  • a local part of atext characters plus ., with no rule on where the dots sit: .ada@, ada.@ and ada..lovelace@ are accepted, following the WHATWG address production rather than RFC 5322;
  • a domain of letter/digit/hyphen labels separated by ., no empty label, no label beginning or ending with -, and no label longer than 63 characters.

Non-ASCII local parts and internationalised domain names are not accepted.

The lower-case rule is a matching rule rather than a syntax one: the same person written two ways would otherwise hash to two tokens and never match.

Declaring all three fields on a transfer:

subject = { tc: "10000000146", phone: "+905342223344", email: "[email protected]" }

produces exactly these tokens, on every platform:

FieldToken
tc0247d10cbc2b05efa49fe02b15ad274d15031abf05a87fffcf461795ccbc6901
phonee47be99e43d11b27bd452dfe825bb979dacc4e9aec9ec7c92dc7547d6a250561
email8823779ef166b203af01b78f5ed7ccfe5e467482bbf1b769333398ebcd7f4acf

You can reproduce any of them yourself:

Terminal window
printf '%s' 'tc:10000000146' | shasum -a 256

The kind prefix is part of the pre-image on every field, always. Without it, a national identification number and a phone number made of the same characters would produce one token, and the two people behind them would be reported as one person across every institution, with nothing raising an error anywhere.

There is no separate subject field on the wire. The tokens ride inside the transaction context, as a nested subject object:

{
"amount": "250.00",
"currency": "TRY",
"subject": {
"tc": "0247d10cbc2b05efa49fe02b15ad274d15031abf05a87fffcf461795ccbc6901",
"phone": "e47be99e43d11b27bd452dfe825bb979dacc4e9aec9ec7c92dc7547d6a250561"
}
}

They are therefore covered by the signature exactly like any other context field you put there, and your backend reads them where it already reads the echoed context. See Backend verification.

A field that was dropped is absent, never present and empty. Above, no email key appears because none was declared or none conformed, and from the wire those two cases are identical.

CaseResult
you declare a subject and at least one field survivesthe typed one replaces your hand-written subject entirely
you declare a subject and nothing survivesyour hand-written subject survives untouched
you declare no subjectyour hand-written subject survives untouched

The replacement is whole rather than field-by-field on purpose: only the typed value is validated, so merging the two would leave one object mixing checked fields with fields that passed no check at all, and nothing downstream could tell which was which.