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

Registration

ComplyOnce staff create your relying party and decide what identity data it may request; there is no self-service sign-up. Everything after that — your client secret, your redirect URIs, and who else from your organisation can change them — you manage yourself in the relying-party portal.

Until your relying party exists and has a secret, nothing authenticates: POST /relying-party/v1/session returns a bare 401, with no error body to read.

Getting into the portal

You sign in to the portal with the ComplyOnce app, the same way your own users will sign in to you. So whoever administers your integration needs the app installed before anything else.

ComplyOnce adds the first person from your organisation: sign in to the portal once, copy your user id from your profile, and send it to ComplyOnce. The portal learns nothing about you beyond that id — no name, no email — which is why it has to be handed over rather than looked up.

After that you invite the rest yourself. An invitation is a one-time link valid for 24 hours, and it is addressed to nobody in particular: whoever opens it and signs in becomes a member. It is exactly as private as the channel you send it over.

What you get

Value Where it comes from
client_id A UUID, assigned when ComplyOnce creates your relying party. It is the HTTP Basic username you authenticate with — not a name you choose.
client_secret Generated in the portal and shown once. ComplyOnce keeps only a hash, so a lost secret can be replaced but never recovered.
name Set by ComplyOnce staff. This is shown on the user's phone when they confirm a login, so it must be the name they know your service by, not an internal project code. Ask for it to be changed if it is wrong.
redirect_uri You add these yourself in the portal, as many as you need. See below.

Because the client_id is a UUID, the credential you build looks like this rather than like a chosen name:

0f9c2b7e-6d41-4b0a-9c8e-5a2f1d3b7e40:8Ky2…

A username that is not a UUID fails authentication outright — it never reaches an endpoint, so there is no error body explaining it. See Authentication.

The redirect URI is matched exactly

The backend compares the redirect_uri you send at the start of each login against the registered value as an exact string — no prefix matching, no wildcards, no normalisation. A mismatch fails with 400 and errorCode: invalid_request.

So these are all different URIs, and only the registered one works:

https://portal.example.com/auth/callback
https://portal.example.com/auth/callback/
http://portal.example.com/auth/callback
https://portal.example.com:443/auth/callback

The portal stores what you type, character for character, for that reason. It rejects only what cannot work as a redirect target: a URI must be absolute and carry its scheme, must not contain a #fragment (which would swallow the authorization code), and must be at most 2000 characters. One relying party may hold up to 20.

Practical consequences:

  • Register every environment separately. Production, staging, and each developer's local URL are distinct entries against the same client_id.
  • Pin your development port. If your dev server picks a different port when the usual one is busy, the redirect URI silently stops matching and login breaks. Configure the port so it fails loudly instead of moving.
  • localhost is not usable for phone-based testing. The user's phone is involved in this flow, and a visitor redirected to localhost lands on their own machine. For testing across devices, register your machine's LAN address (http://192.168.1.50:5175/auth/callback).

Identity data you may request

A login returns a subjectId by default. If you also want identity attributes or predicates, ask ComplyOnce: which keys a relying party may request is a staff decision, not a self-service one, and it is recorded as a per-client allow-list. A login may then request only from that set; asking for anything else fails with invalid_scope.

Grantable key Kind
name, personal_code, date_of_birth, country Attribute
age_at_least, country_in Predicate

Grants are by type, so being allowed age_at_least allows it for any number of years. The and / or composite predicates need no grant of their own. See Identity properties for how the request and the returned data are shaped.

Keeping the secret

The client_secret is sent from your server to the ComplyOnce backend as the HTTP Basic password, on every call — see Authentication. It must not appear in browser JavaScript, in a mobile app binary, in a query string, or in a log. Load it from the environment or a secret manager — not from source control.

You can generate a new one in the portal at any time, which is what to do if it leaks. Rotation is not overlapping: there is one secret per relying party, so the old one stops working the moment the new one appears. Between those two moments every call you make returns 401, so treat it as a short deployment rather than a background task — have the new value ready to roll out before you press the button.

The legacy relying_party.token is gone

If you have an older ComplyOnce integration, you may hold a token UUID. It no longer exists: the column has been dropped, and the push-based signing endpoint that used it now authenticates with client_id / client_secret like everything else — see Signing flow.

If that token was your only credential, you need a client_id and client_secret before anything will authenticate.

Configuration checklist

Once registered, your server needs:

COMPLYONCE_CLIENT_ID=0f9c2b7e-6d41-4b0a-9c8e-5a2f1d3b7e40
COMPLYONCE_CLIENT_SECRET=               # from a secret store, never in git
COMPLYONCE_BACKEND_URL=https://…         # every ComplyOnce call goes here
PUBLIC_BASE_URL=https://portal.example.com

PUBLIC_BASE_URL matters more than it looks: your redirect URI is derived from it, so it has to be the origin the visitor's browser reaches you on. Deriving it from the request host instead makes the redirect URI vary, and a varying redirect URI cannot exact-match a registered one.

Next: Authentication, then the login flow.