Authentication
Two different things get authenticated in a ComplyOnce integration, and it helps to keep them apart:
- Your service to ComplyOnce — every API call you make carries your client credentials. That is this page.
- The user to you — the person proves who they are on their phone, and you learn the result. That is the login flow.
HTTP Basic on every call
Every endpoint under /relying-party/ requires HTTP Basic authentication. The
username is your client_id — a UUID assigned to you, not a name you chose — and
the password is your client_secret, both from
registration.
Authorization: Basic MGY5YzJiN2UtNmQ0MS00YjBhLTljOGUtNWEyZjFkM2I3ZTQwOjhLeTJRcFh2VG43Um1CNGRMdzlzRmhaYzBqVmdLZTFV
That value is Basic followed by standard base64 of client_id:client_secret.
Standard base64, not the base64url used for code_challenge and properties — a
secret containing + or / is encoded normally here.
curl -u "$COMPLYONCE_CLIENT_ID:$COMPLYONCE_CLIENT_SECRET" …
The credential is what identifies you, so no request body carries a client_id.
Which calls need it
All of them:
| Endpoint | Called during |
|---|---|
POST /relying-party/v1/session |
Login flow step 1 |
GET /relying-party/v1/session/{sessionId}/status |
Login flow step 3 |
POST /relying-party/v1/session/{sessionId}/completion |
Login flow step 3 |
POST /relying-party/v1/session/token |
Login flow step 5 |
POST /relying-party/v1/create-signing-request |
Legacy push signing — see Signing flow |
There is no public-client variant of any of them: a browser cannot call the endpoints above, because it cannot hold your secret.
A browser can reach the /login-ui/v1/session endpoints, which exist for the
ComplyOnce-hosted login page. They authenticate with a cookie the page obtains by
spending a one-time handoff token your server was given, so no client credential
is involved there either. See
the hosted login page. If you host the
waiting page yourself, everything a user's browser touches is served by you.
Node.js
export function basicAuthorization() {
const credential = `${config.clientId}:${config.clientSecret}`
return `Basic ${Buffer.from(credential, 'utf8').toString('base64')}`
}
Java
String basicAuthorization() {
String credential = clientId + ":" + clientSecret;
return "Basic " + Base64.getEncoder().encodeToString(credential.getBytes(StandardCharsets.UTF_8));
}
When it fails
Credentials are checked before your request reaches the endpoint, which changes the shape of the failure:
HTTP/1.1 401
WWW-Authenticate: Basic realm="Realm"
The body of a 401 is not the ComplyOnce error envelope and never carries an
errorCode. Code that reads body.errorCode on every non-2xx response will throw
on the one failure most likely to happen on your first deployment — branch on the
status first.
An unregistered client_id fails here too, rather than reaching an endpoint. So
invalid_client is not a response you will see.
Four causes, all yours to fix rather than retry:
| Cause | Fix |
|---|---|
No Authorization header |
Your HTTP client is dropping it — some drop headers when following a redirect. |
Wrong client_secret |
Check which environment's secret you loaded. |
client_id is not a UUID |
It is the id assigned to your relying party, not a name. A malformed username fails before anything looks it up. |
Unknown client_id |
Check you are pointing at the environment your relying party exists in — see Registration. |
A session belongs to the client that created it
status and completion compare the session against the authenticated
client_id. Acting on a session created under a different client returns 404
session_not_found — deliberately not 403, so one client cannot use the
difference between the two to discover another client's session ids.
In practice this means the credential and the sessionId must travel together:
the server that created a session is the only one that can poll or complete it.
Handling the secret
The client_secret now appears on every ComplyOnce call rather than just one, so
it has more chances to leak than it used to:
- Load it from the environment or a secret manager, never from source control.
- Keep it server-side. It cannot appear in browser JavaScript, in a mobile app binary, or in a URL.
- Check that your HTTP client does not log request headers. An
Authorizationheader in an access log is a leaked secret. - Do not put it in a query string even for testing — query strings are logged by proxies and servers that never log bodies or headers.
If it leaks, generate a new one in the relying-party portal. There is one secret at a time, so the old one dies the moment the new one is issued — see Registration.
Next: the login flow.