Receipts: signed proof of a verdict
Every run-tests submission that finishes gets a receipt: a statement, signed by OnlineCompiler.io, that code with this hash was run against tests with this hash under these settings on this compiler image, and produced this verdict and score on this date. Anyone with the public key can verify it offline.
A receipt does not prove who wrote the code, or that they wrote it without help. It proves what was run, against what, and what came out, so it is evidence for an assessment, a placement test or a hiring pipeline that the result you recorded is the result OnlineCompiler.io produced and has not been edited since.
It comes back as receipt in the run-tests response, in a submission's lookup and in its webhook. It is null when the submission failed on our side, or when no receipt could be signed.
Fields
| Field | Description |
|---|---|
| receipt | The receipt's id, like rcpt_7Hq2Zp4Wn8Ks1Ab6Ct3Dv5Ex. |
| format | 1. It changes only if the way a receipt is built changes. |
| submission | The submission it is for. |
| account | An opaque reference to the account that sent it, like acct_3k9Fj2Lq8Rt5Wm1Z. The same on every receipt of the account; it is not the account id and holds no email or name. |
| external_ref | The request's external_ref, or null. |
| issued | When it was issued, UTC, to the second. |
| compiler | id and version, and the image: the digest of the container image the code ran in, or null when it could not be read. |
| code_sha256 | SHA-256 of the code as UTF-8 bytes, in hex. |
| tests_sha256 | SHA-256 of the tests, salted with the submission's hash_salt, in the canonical form below. |
| files_sha256 | SHA-256 of the input files, salted the same way, in the canonical form below; null when the request had none. |
| settings | checker, tolerance, scoring, time_limit and stop_on_failure, as used. |
| verdict, score, passed, total | The overall result, as in the response. |
| tests | Per test: verdict and hidden, and time for a visible test. Nothing else: no input, expected output or printed output. |
| key_id | The signing key's id: look it up in the key list. |
| signature | Base64 Ed25519 signature over the canonical JSON of every other field. |
Hidden tests carry only verdict and hidden, here as everywhere.
The hashes
Hash your own copy of the request to show a receipt belongs to it. code_sha256 is the SHA-256, in hex, of the code as UTF-8 bytes. The tests and files hashes are salted: every submission has its own random hash_salt, which only your account's lookup, GET /api/submissions/{id}/, returns. It is not in the receipt, so nobody can check a guess at your hidden tests against a receipt you share. tests_sha256 is the SHA-256, in hex, of the canonical JSON (below) of {"salt": hash_salt, "tests": [...]}: the list of tests in the order sent, each as {"input", "expected_output", "hidden", "weight"}, with the defaults filled in: input is "", hidden is false and weight is 1. files_sha256 is the SHA-256 of the canonical JSON of {"files": {...}, "salt": hash_salt}, where files maps each file's name to the SHA-256, in hex, of its decoded content. Keep hash_salt with your copy of the request: the lookup returns it only while the submission is kept.
import base64, hashlib, json, urllib.request
import rfc8785
request = json.load(open("request.json")) # the body you sent
receipt = json.load(open("receipt.json"))
sha = lambda data: hashlib.sha256(data).hexdigest()
# The salt comes from your account's lookup of the submission.
lookup = urllib.request.Request("https://api.onlinecompiler.io/api/submissions/" + receipt["submission"] + "/",
headers={"Authorization": "YOUR_API_KEY"})
salt = json.load(urllib.request.urlopen(lookup))["hash_salt"]
tests = [{"input": t.get("input", ""), "expected_output": t["expected_output"],
"hidden": t.get("hidden", False), "weight": t.get("weight", 1)} for t in request["tests"]]
print(sha(rfc8785.dumps({"salt": salt, "tests": tests})) == receipt["tests_sha256"])
print(sha(request["code"].encode()) == receipt["code_sha256"])
files = {f["name"]: sha(base64.b64decode(f["content"]) if f.get("encoding") == "base64" else f["content"].encode())
for f in request.get("files", [])}
print((sha(rfc8785.dumps({"files": files, "salt": salt})) if files else None) == receipt["files_sha256"])Canonical JSON
Signatures and hashes are over canonical JSON as RFC 8785 defines it: object keys sorted, no whitespace between tokens, UTF-8 output with non-ASCII text left as it is, and numbers written as ECMAScript writes them (1e-06 is 0.000001 and 100.0 is 100). Use a library for it, as below, rather than json.dumps or JSON.stringify alone.
The public keys
Receipts are signed with Ed25519. The public keys are at https://api.onlinecompiler.io/.well-known/receipt-keys.json, which needs no API key and may be cached for 60 minutes. Each has a key_id, the base64 of its raw 32-byte public_key, and the algorithm. A receipt names its key in key_id. When we rotate the signing key, new receipts name the new key. Keys that no longer sign stay in the list, so older receipts keep verifying. If you meet a key_id you do not have, fetch the list again.
{"keys": [{"key_id": "oc-2026-11", "algorithm": "Ed25519", "public_key": "fClwrhauAHaFRtbNP9rfQJtsj5dIsAFwDeFRsQ3oyas="}]}Verifying a receipt
Remove signature, write the rest as canonical JSON and check the signature over those bytes with the key the receipt names. Each program prints valid, and prints invalid once any field has been changed. Save the receipt object as receipt.json (the public API wraps it as {"receipt": {...}}, so save what is inside).
pip install rfc8785 cryptographyimport base64, json, urllib.request
import rfc8785
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
receipt = json.load(open("receipt.json"))
keys = json.load(urllib.request.urlopen("https://api.onlinecompiler.io/.well-known/receipt-keys.json"))["keys"]
key = next(k for k in keys if k["key_id"] == receipt["key_id"])
message = rfc8785.dumps({k: v for k, v in receipt.items() if k != "signature"})
public = Ed25519PublicKey.from_public_bytes(base64.b64decode(key["public_key"]))
try:
public.verify(base64.b64decode(receipt["signature"]), message)
print("valid")
except InvalidSignature:
print("invalid")npm i canonicalizeimport crypto from 'node:crypto';
import { readFileSync } from 'node:fs';
import canonicalize from 'canonicalize';
const receipt = JSON.parse(readFileSync('receipt.json', 'utf8'));
const { keys } = await (await fetch('https://api.onlinecompiler.io/.well-known/receipt-keys.json')).json();
const key = keys.find((k) => k.key_id === receipt.key_id);
const { signature, ...unsigned } = receipt;
const x = Buffer.from(key.public_key, 'base64').toString('base64url');
const publicKey = crypto.createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x }, format: 'jwk' });
const ok = crypto.verify(null, Buffer.from(canonicalize(unsigned)), publicKey, Buffer.from(signature, 'base64'));
console.log(ok ? 'valid' : 'invalid');Sharing a receipt
POST https://api.onlinecompiler.io/api/submissions/{id}/share/ makes a submission's receipt public and returns its url. Anyone with the link can read the receipt there, or as JSON at GET /api/receipts/{receipt id}/, with no API key. The page shows the hashes, the verdicts and the external_ref, and never the code, the tests, the outputs or the metadata.
external_ref and the account reference. Use an opaque external_ref such as an attempt id, never an email address or a name.Retention and deletion
Submissions, and so shared receipt links, are kept for a period that depends on the plan: Free 90 days, Starter 365 days, Pro 730 days, Business 730 days. After that the link answers 404; a receipt you saved still verifies.
DELETE /api/submissions/{id}/ deletes a submission, its tests' stored data and any output files, and its receipt stops being public. See Looking up, deleting and sharing.
For who uses receipts, and how signed webhooks and signed results compare with Sphere Engine, Judge0 and Piston (checked against their public documentation on 4 November 2026), see the coding assessment API page.
Questions
- Does a receipt prove who wrote the code?
- No. A receipt does not prove who wrote the code, or that they wrote it without help. It proves what ran and what came out.
- Can I verify a receipt without calling OnlineCompiler.io?
- Yes, once you have the public key. The signature is checked locally against the key list, so a receipt you stored stays verifiable even after the submission is deleted.
- Is the code in the receipt?
- No, only its SHA-256. Keep the code yourself and hash it to show a receipt belongs to it.