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

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 Authorization header 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.