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

Signing flow

Signing works like login: you create a session, the user scans its QR code and confirms on their phone. The differences are that you say what is to be signed when you create the session, and you pull the result from the session instead of receiving an authorization code through the browser.

Only the SHA-512 hash of your data ever reaches ComplyOnce. The document stays with you, and what comes back is a detached ASiC-E container: the signature and a manifest naming your data by hash, never the data itself.

The flow at a glance

  1. Your server hashes the document and creates a signing session, getting back a sessionId.
  2. Your page renders the QR code built from that sessionId.
  3. The user scans it. The app shows your registered name and your title, so they see who is asking and what for.
  4. The user confirms with their signing PIN.
  5. Your server polls the session, sees COMPLETED, and fetches the result.
  6. You store the container. We delete it 30 minutes after signing.

There is no code, no redirect_uri and no PKCE in this flow. Those exist to protect an authorization code travelling through a browser; signing has no such code, and nothing sensitive travels through the browser at all.

Step 1: Create the session

Hash the exact bytes the user is agreeing to, with SHA-512, hex-encoded:

curl -X POST https://<backend-host>/relying-party/v1/session \
  -u "$COMPLYONCE_CLIENT_ID:$COMPLYONCE_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "signature_type": "SIGNING",
    "signed_data_hash": "7947b485a40b4c27984f2261c6624d9d80f70bd2c862d9107335a6001f5ea9ce95c7a56752a31016188284289185e2dc4d5a26e35f4b3ef057c11e2b65489c96",
    "signed_data_name": "contract.pdf",
    "title": "Sign the rental agreement"
  }'
{
  "sessionId": "59a19705-0e58-441d-98aa-522c8355f9a5",
  "expiresAt": 1787056921
}

signed_data_name is the name your data carries inside the container manifest, and is the entry name you will write when you assemble the container with the document for archiving. It defaults to detached-file and may not contain path separators. title is free text shown to the signer, labelled in the app as coming from you. It is the main thing the signer reads before approving, so word it as the sentence they are agreeing to — and note it is not covered by the signature; only the hash is. Keep your own record of what you displayed.

To restrict a session to one known user, add subject_id with the subjectId you learned from a previous login. Anyone else who scans is refused, and the session stays open for the right person.

Naming the signer does not reach them on its own. Add push: true as well and ComplyOnce sends the session to that person's devices, so their app opens on the signing screen and step 2 below is unnecessary — there is no QR code for anyone to scan. Pushing needs a subject_id, since there would otherwise be nobody to push to. The response tells you what happened in pushStatus; on NO_DEVICE, show the QR code as usual.

Step 2: Show the QR code

The QR payload is the same as for login:

complyonce://app/scan?sessionId=<uuid>&returnUrl=<url-encoded return address>

The hash is not shown to the user, on your page or in the app: a 128-character string is not something a person can compare, and displaying it invites a check nobody performs. What the signer sees is your registered name and your title, so make the title say plainly what they are agreeing to.

Step 3: Poll until it is signed

curl https://<backend-host>/relying-party/v1/session/$SESSION_ID/status \
  -u "$COMPLYONCE_CLIENT_ID:$COMPLYONCE_CLIENT_SECRET"

A push-initiated signing session can sit for minutes while the signer finds their phone, which is a long time to keep asking. ComplyOnce can post to your server when it changes instead — see Webhooks.

PENDING → SCANNED → COMPLETED. COMPLETED means the signature exists and the result can be fetched. CANCELLED means the signer declined, left the flow, or locked their device with wrong PINs; EXPIRED means the session ran out before the user confirmed. Both are terminal — create a new one.

Step 4: Fetch the result

curl https://<backend-host>/relying-party/v1/session/$SESSION_ID/result \
  -u "$COMPLYONCE_CLIENT_ID:$COMPLYONCE_CLIENT_SECRET"
{
  "container": "UEsDBBQACAgIAAAAIQ…",
  "signedDataHash": "7947b485a40b4c27…65489c96",
  "signedDataName": "contract.pdf",
  "signingTime": 1787056801,
  "name": "Mari Mets",
  "personalCode": "38001085718",
  "dateOfBirth": "1980-01-08",
  "country": "EE"
}

container is the base64 ASiC-E. Fetch it as many times as you need — a failed download or a crashed worker is not a lost signature.

The identity fields are there for convenience. What actually binds the signature to a person is the signing certificate inside the container; verify against that, not against this JSON.

Store the container. Signing results are deleted 30 minutes after signing. ComplyOnce is not your archive: after that window the session is gone and both status and result return 404. If you miss it, the user has to sign again.

Verifying and archiving

The container is detached: it holds the hash of your document, not the document. To validate it — or to keep something a third party can validate years from now — write your document into the container under signed_data_name, or hand both to a validator that accepts detached content.

The signer's identity is in the certificate: their name, their national identifier in the subject serialNumber, and their country. A certificate whose serialNumber begins with DEMO belongs to a demo identity created for testing — it is not a verified person, and you should refuse it in production.

Errors

Status errorCode Cause
400 invalid_request signed_data_hash missing or malformed, or signing fields sent on an authentication session
404 session_not_found Unknown session, another client's session, or one already deleted after its retention window
409 not_signed Fetched the result before the session reached COMPLETED

Login is documented separately — see the login flow.