Subject Identity
Declare whose transaction you are signing, without the identifier ever leaving the device.
Overview
Section titled “Overview”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.
Values are dropped, never corrected
Section titled “Values are dropped, never corrected”A value that does not conform is discarded. It is not repaired, reformatted or normalised.
| You supply | What 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.
The accepted forms
Section titled “The accepted forms”tc: Turkish national identification number
Section titled “tc: Turkish national identification number”- exactly 11 ASCII digits,
0–9only; - 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.
phone: E.164
Section titled “phone: E.164”- 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–Zis dropped, before the grammar is even considered; - exactly one
@; - a local part of
atextcharacters plus., with no rule on where the dots sit:.ada@,ada.@andada..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.
Worked example
Section titled “Worked example”Declaring all three fields on a transfer:
subject = { tc: "10000000146", phone: "+905342223344", email: "[email protected]" }produces exactly these tokens, on every platform:
| Field | Token |
|---|---|
tc | 0247d10cbc2b05efa49fe02b15ad274d15031abf05a87fffcf461795ccbc6901 |
phone | e47be99e43d11b27bd452dfe825bb979dacc4e9aec9ec7c92dc7547d6a250561 |
email | 8823779ef166b203af01b78f5ed7ccfe5e467482bbf1b769333398ebcd7f4acf |
You can reproduce any of them yourself:
printf '%s' 'tc:10000000146' | shasum -a 256The 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.
How the tokens travel
Section titled “How the tokens travel”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.
If your context already has a subject key
Section titled “If your context already has a subject key”| Case | Result |
|---|---|
| you declare a subject and at least one field survives | the typed one replaces your hand-written subject entirely |
| you declare a subject and nothing survives | your hand-written subject survives untouched |
| you declare no subject | your 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.