Commitments

Some challenges are drawn as a secret program before the camera opens. At that moment we publish a SHA-256 digest of the program, and you receive it in the session create response. The program itself is released one step at a time while the check runs.

Once the session has finished, you can open the commitment: we return the program and the random salt behind each digest, and you recompute the hash yourself. If it matches the digest you were given at the start, the challenge was fixed before your user was shown anything, and a recording made in advance could not have contained a response to it. You do not have to take our word for that.

Where the digest comes from

A challenge that commits carries commitmentHash in its params in the challengeSequence of the session create response. Store it with the sessionId if you want to check the opening against a value you held before the session ran, rather than against the copy in the opening itself.

Not every session draws a committing challenge. A session with none has nothing to open.

Open a session's commitments

curl https://machine.cognau.com/api/v1/cockpit/sessions/6a7de599a5244ea6a3e65109/commitments \
  -H "Authorization: Bearer $COGNAU_SECRET_KEY"

Authenticate with your secret API key, from your server. Only sessions that belong to your account are returned; any other id is a 404.

When you can call it

Only once the session can no longer run a challenge: after it has been scored, passed, failed, sent to review or expired. While it is still being created or is running, the call is refused, because opening a commitment early would hand over the answer to a challenge that has not been asked yet:

{
  "success": false,
  "error": "This session is still running. Commitments open once it has finished.",
  "code": "session_live"
}

That is a 409, not a 403. You are entitled to the opening, just not yet. Retry after the verification.completed webhook arrives.

Response

{
  "success": true,
  "data": {
    "sessionId": "6a7de599a5244ea6a3e65109",
    "status": "verified",
    "verification": {
      "algorithm": "sha256",
      "preimage": "JSON.stringify(program) + \":\" + salt"
    },
    "commitments": [
      {
        "type": "…",
        "index": 2,
        "commitmentHash": "2b0496dc12dffe41619a8885dba46976a9c2ffd57af3b56d514dbcce07ba2c25",
        "program": ["a", "b", "c"],
        "salt": "0f1e2d3c4b5a69788796a5b4c3d2e1f0",
        "matches": true
      }
    ]
  }
}
Field Meaning
verification How to recompute each digest. It travels with the data so the recipe cannot drift from it.
commitments One entry per challenge that committed. An empty list is an ordinary answer.
type, index Which challenge in the sequence the entry belongs to.
commitmentHash The digest published when the session was created.
program The secret program, revealed now that it can no longer be acted on. Treat it as opaque JSON.
salt The random salt the digest was taken over.
matches Our own recomputation of the digest, compared with commitmentHash.

matches is always true in normal operation. It is computed rather than assumed so that a stored opening that does not match shows up in the response instead of waiting for you to find it by hand. It is a convenience, not the proof: the proof is your own recomputation below.

The opening returns only what your user was shown. It never includes how the response was scored.

Recompute the digest

The preimage is the program serialised as compact JSON, then a colon, then the salt. The program in the example above is a made-up placeholder; use whatever the response carries.

JavaScript (Node)

import { createHash } from 'node:crypto';

function verifyOpening(c, digestYouStored = c.commitmentHash) {
  const digest = createHash('sha256')
    .update(JSON.stringify(c.program) + ':' + c.salt)
    .digest('hex');
  return digest === digestYouStored;
}

// ["a","b","c"]:0f1e2d3c4b5a69788796a5b4c3d2e1f0
// -> 2b0496dc12dffe41619a8885dba46976a9c2ffd57af3b56d514dbcce07ba2c25

Python

import hashlib, json

def verify_opening(c, digest_you_stored=None):
    preimage = json.dumps(c["program"], separators=(",", ":"), ensure_ascii=False) + ":" + c["salt"]
    digest = hashlib.sha256(preimage.encode("utf-8")).hexdigest()
    return digest == (digest_you_stored or c["commitmentHash"])

Two details make the Python match byte for byte: separators=(",", ":") removes the spaces json.dumps adds by default, and ensure_ascii=False keeps characters as they are instead of escaping them. Both languages keep object keys in the order they were parsed, so re-serialising the parsed program reproduces the original bytes.


Reading this as an agent? The raw Markdown is at /docs/commitments.md.