Identity properties
A login gives you a subjectId and nothing else by default. If you need more —
the user's name, or a check such as "is at least 18" — you can ask for it as
part of the login, and receive it in the token response alongside the subjectId.
Two kinds of thing are requestable, and the distinction is the point:
- Attributes — an actual value.
name→"Demo User",country→"EE". - Predicates — a true/false check that ComplyOnce evaluates, returning only
the boolean.
age_at_least: 18→truetells you the user is at least 18 without disclosing their date of birth.
Ask for the least that answers your question. If all you need is an age gate, a
predicate leaks nothing; requesting date_of_birth and computing the age
yourself hands you a liability you did not need.
Attributes
| Key | Value | Notes |
|---|---|---|
name |
string | Full name as held by ComplyOnce. |
personal_code |
string | National identification code. |
date_of_birth |
string | ISO YYYY-MM-DD. |
country |
string | Country code as held by ComplyOnce. |
Values are always strings. A requested attribute with no stored value is
omitted from the response rather than returned as null — see
Results.
Predicates
Two leaf predicates exist:
| Type | Parameter | True when |
|---|---|---|
age_at_least |
years (positive integer) |
The user's age, from their date of birth, is at least years. |
country_in |
countries (non-empty array of strings) |
The user's country is one of countries, compared case-insensitively. |
{ "type": "age_at_least", "years": 18 }
{ "type": "country_in", "countries": ["EE", "LV", "LT"] }
Composite predicates
and and or combine other predicates into one, so a single request can
express a rule rather than a checklist:
| Type | Parameter | True when |
|---|---|---|
and |
of (non-empty array of predicates) |
Every nested predicate is true. |
or |
of (non-empty array of predicates) |
At least one nested predicate is true. |
{
"type": "or",
"of": [
{ "type": "and", "of": [ { "type": "country_in", "countries": ["EE", "LV", "LT"] },
{ "type": "age_at_least", "years": 18 } ] },
{ "type": "and", "of": [ { "type": "country_in", "countries": ["DE", "FR"] },
{ "type": "age_at_least", "years": 21 } ] }
]
}
That asks "over 18 in the Baltics, or over 21 in Germany or France" and returns
a single true/false. A composite discloses only its own result — never
which branch decided it — so you learn one boolean, not one per leaf. There is
deliberately no not: evaluation is fail-closed (below), and negating a
fail-closed answer would turn missing data into true.
A request may nest up to 5 levels deep and contain at most 50 predicates
in total. An empty and or or is rejected rather than treated as vacuously
true.
Requesting properties
Send a properties field in the session-creation body from
step 1 of the login flow. Its
value is the JSON object below, base64url-encoded into a string:
{
"attributes": ["name", "country"],
"predicates": [ { "type": "age_at_least", "years": 18 } ]
}
Both keys are optional; a request may carry attributes only, predicates only, or both. The request is bound to the session at creation, and the user's phone shows exactly what was asked for before they enter their PIN — entering it consents to the whole request. Because the request is fixed before the QR code is ever shown, what the user approves cannot differ from what you asked for.
function encodeProperties(properties) {
return Buffer.from(JSON.stringify(properties), 'utf8').toString('base64url')
}
export function createLoginSession({ state, codeChallenge, properties }) {
const body = {
redirect_uri: `${config.publicBaseUrl}/auth/callback`,
state,
signature_type: 'AUTHENTICATION',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
}
if (properties) {
body.properties = encodeProperties(properties)
}
// …POST it with your Basic credential, as in Login flow step 1
return body
}
The value is base64url of the UTF-8 JSON, carried as a plain JSON string field. It is not percent-encoded and not signed — it no longer travels through the browser, so nothing between you and ComplyOnce sees or can alter it.
The allow-list
You cannot request arbitrary data. ComplyOnce holds a set of allowed property
keys per client_id, granted at registration, and a
request may only draw from it. The grantable keys are the attribute keys
(name, personal_code, date_of_birth, country) and the leaf predicate
types (age_at_least, country_in); and and or need no grant of their own,
since they disclose nothing and their leaves are checked individually.
Requesting anything outside your allow-list fails session creation with
invalid_scope — the login never starts, rather than quietly returning less
than you asked for. Consent is all-or-nothing: the user approves the whole
request with their PIN or the login does not complete. A client with no granted
keys cannot request any properties.
Results
When you requested any properties, the token response
carries attributes and predicates next to subjectId:
{
"subjectId": "8f14e45f-ceea-4a1b-9f2c-3d5b7a081c62",
"attributes": { "name": "Demo User", "country": "EE" },
"predicates": [
{ "type": "age_at_least", "years": 18, "result": true },
{ "type": "country_in", "countries": ["EE", "LV", "LT"], "result": true }
]
}
attributesis an object keyed by attribute key. A requested attribute with no stored value is absent, so four requested can return three — read it by key, not by assuming every key is present.predicatesis an array in request order. Each entry echoes itstypeand its parameters alongside the booleanresult, so you match an answer to its question rather than to an array position. A composite echoes onlytypeandresult.
A plain authentication — no properties on the redirect — returns just
{ "subjectId": … }, exactly as before. attributes and predicates are absent,
not empty.
Predicates fail closed
result: false means not proven, not proven false. If the data behind a
predicate is missing, the answer is false — never an error, never null. So a
false from age_at_least covers both "under 18" and "date of birth unknown",
and you cannot tell which. Only true is actionable. User-facing copy should say
"we could not verify that you meet the requirement", not "you are too young".
Errors
Both surface on POST /relying-party/v1/session, so they happen on your server
before any page is shown to the user. Treat them as configuration errors and
alert, rather than showing the user a failed login.
errorCode |
Cause |
|---|---|
invalid_request |
properties is not valid base64url JSON, a parameter is unsound (years not positive, empty countries, empty and/or), or the tree exceeds the depth or node limit. |
invalid_scope |
An attribute or leaf predicate is not in your allow-list. |
What is not here yet
- Results are returned as plain JSON in the token response. A signed OpenID
Connect
id_tokenand a JWKS endpoint are planned but not available, so treat the response as trusted because it came over your authenticated back-channel token call, not because it is individually signed. - The request is not signed, and does not need to be while it travels only from your server to ComplyOnce over an authenticated call. ComplyOnce additionally bounds what can be requested with your per-client allow-list, and every predicate echoes its parameters in the result, so you can confirm an answer matches the question you posed.
For the endpoint-level contract, see the API reference.