Unstable API: endpoints and payloads are still changing. Expect breaking changes without notice, and plan to update your integration.

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: 18true tells 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 }
  ]
}
  • attributes is 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.
  • predicates is an array in request order. Each entry echoes its type and its parameters alongside the boolean result, so you match an answer to its question rather than to an array position. A composite echoes only type and result.

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_token and 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.