What is a receipt?

A receipt is the signed record Botrite Observe returns after it observes a URL. It captures what URL was requested, when the observation happened, what fetch and normalization rules were applied, whether the content changed relative to the prior observation of that URL, and a signature that lets you verify the receipt independently.

A receipt is evidence of an automated observation. It is not a certification of the page, the publisher, or the truth of the page’s contents.

Request

Use POST /v1/observe to request one observation of a public URL.

POST /v1/observe

POST https://api.lattiq.ai/v1/observe Authorization: Bearer ltq_live_<your_api_key> Content-Type: application/json {"url": "https://example.com/article"}

Request body

FieldTypeRequiredDescriptionExample
urlstringYesThe public URL to observe.https://example.com/article

Request headers

HeaderRequiredDescription
AuthorizationYesBearer token for your Botrite Observe API key.
Content-TypeYesSend JSON with application/json.

Example request

curl -X POST https://api.lattiq.ai/v1/observe \ -H "Authorization: Bearer ltq_live_1234567890example" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/article"}'

Response — v0.5.0 receipt

On success, the API returns a JSON receipt. The receipt is a single document with a 12-field signed payload and a 3-field signing envelope.

Success body (200 OK)

{ "chain_state": "CHANGED", "content_hash": "sha256:4fd708c6f57d9d58dbec4bf3b6f0b3560f5f3b1c82f0b2ac3f8f9f7b2c6cfd52", "disclaimer_hash": "sha256:a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef001122334455667788", "fetch_policy": { "final_url": "https://example.com/article", "max_bytes": 5242880, "mode": "http", "timeout_ms": 8000 }, "normalization": { "method": "html", "spec_hash": "sha256:f1e2d3c4b5a697886950413223100415263748596071829304b5c6d7e8f9a0b1", "version": "v1" }, "observation_profile_hash": "sha256:9f8e7d6c5b4a39281716151413121110ffeeddccbbaa99887766554433221100", "observed_at": "2026-05-01T13:42:11Z", "previous_receipt_id": "5d93b4b4-4537-4d9e-8e0e-59c8e2db9f0d", "receipt_id": "0b4a570c-bf79-49d7-9c0a-f4fd4eb5d25a", "result": "SUCCESS", "schema_version": "0.5.0", "url": "https://example.com/article", "signing": { "algorithm": "ed25519", "key_id": "lattiq-1", "signature": "<ed25519_signature_base64>" } }

Receipt fields

Field nameTypeDescription
chain_statestring enumWhether this observation is the first one for the URL, matches the prior content, differs from the prior content, or cannot be compared.
content_hashstringSHA-256 hash of the normalized content bytes observed for this receipt.
disclaimer_hashstringSHA-256 hash of the legal disclaimer text in force when the receipt was signed.
fetch_policy.final_urlstringFinal destination URL after redirects, in canonicalized form.
fetch_policy.max_bytesintegerMaximum number of bytes the observation fetch was allowed to read.
fetch_policy.modestringFetch mode used for the observation.
fetch_policy.timeout_msintegerMaximum fetch time in milliseconds.
normalization.methodstringNormalization method used before hashing the content.
normalization.spec_hashstringSHA-256 hash of the normalization rules document used for this receipt.
normalization.versionstringVersion label for the normalization rules used for this receipt.
observation_profile_hashstringSHA-256 hash binding the full observation methodology used for the receipt.
observed_atstringUTC timestamp when the observation was recorded.
previous_receipt_idstring or nullThe prior receipt in the observation chain for this URL, or null when there is no linked prior receipt.
receipt_idstringUnique identifier for this receipt.
resultstring enumOutcome of the observation attempt.
schema_versionstringReceipt schema version.
urlstringThe original URL you asked Botrite Observe to observe.
signing.algorithmstringSignature algorithm used for the receipt.
signing.key_idstringOpaque signing key identifier used to select the trust anchor during verification.
signing.signaturestringSignature over the signed receipt payload.

Field semantics

result

result tells you whether the observation completed normally. SUCCESS means the observation finished and the receipt contains a content hash for the normalized content that was observed. Any non-SUCCESS value means the observation attempt was still recorded, but the fetch or normalization path did not complete in the usual way. In plain language: the system reached a definite outcome, signed that outcome, and told you why a normal content comparison may not be available.

result valueMeaning
SUCCESSThe URL was observed successfully and the receipt contains a normal content hash.
ERRORA general error prevented a normal observation outcome.
TOO_LARGEThe response exceeded the allowed fetch size limit.
UNREACHABLEThe URL could not be reached within the allowed network policy or timeout.
BLOCKED_BY_ROBOTSAccess was denied by a robots policy.
BLOCKED_BY_SITEThe site blocked the request directly.
INVALID_RESPONSEThe response could be fetched but could not be interpreted as a valid observation input.
PARTIAL_FETCHOnly part of the response could be fetched, so the observation was incomplete.
NORMALIZATION_FAILEDThe content was fetched, but the normalization step failed before a normal content hash could be produced.

When you build client logic, treat SUCCESS as the normal case and treat every other value as a completed observation with an error outcome rather than as an unsigned or missing result.

chain_state

chain_state tells you how this receipt relates to the prior observation of the same URL under the same observation methodology.

chain_state valueCustomer interpretation
UNCHANGEDObservation recorded — content unchanged since the previous observation of this URL.
CHANGEDObservation recorded — content changed since the previous observation.
GENESISFirst observation of this URL.
UNKNOWNObservation recorded — comparison state could not be determined.

UNKNOWN should be rare. It means the observation itself was recorded, but a clean content comparison to the prior receipt could not be made.

previous_receipt_id

previous_receipt_id links receipts into a backward chain. When it is a non-null value, this receipt points to the immediately previous linked receipt for the same URL. When it is null, the receipt starts a chain segment. That usually means this is the first recorded observation for that URL, or the chain intentionally restarted because the comparison basis changed. To walk the chain, start from the current receipt_id, read previous_receipt_id, then repeat with the earlier receipt until you reach null.

observation_profile_hash

observation_profile_hash exists so the receipt is cryptographically bound to the full observation methodology that produced it. That includes the fetch and normalization recipe needed to make the observation reproducible as a signed claim, without requiring every internal parameter to be expanded into the public receipt body. In practice, this field lets a verifier distinguish between “same URL, same method” and “same URL, different method,” which is important when comparing receipts across time.

disclaimer_hash

disclaimer_hash points to the exact legal disclaimer text in force when the receipt was signed. This keeps the legal framing tamper-evident: the receipt does not just say a disclaimer existed, it binds the exact disclaimer bytes by hash. If you archive receipts for compliance or audit use, archive the corresponding disclaimer text alongside them.

normalization.spec_hash and normalization.version

normalization.version tells you which named normalization rules were used, and normalization.spec_hash lets you verify the exact rules document by hash. Together, they answer a practical question customers often have: “What content-processing rules were in force when this hash was computed?” If you compare receipts across time, these fields tell you whether you are comparing hashes derived under the same normalization rules.

fetch_policy.final_url

fetch_policy.final_url is the destination URL after redirects, expressed in canonicalized form. This matters because the URL you requested and the URL that ultimately served the content are not always the same. If a site redirects from one path, host, or locale to another, url shows what you asked for and fetch_policy.final_url shows where the observation actually landed.

signing.key_id

signing.key_id is an opaque key identifier. Use it as a lookup key against KEY_FINGERPRINTS.txt during verification (see the Trust anchor section). Do not infer meaning from the identifier itself. The trust decision comes from matching the receipt’s key identifier to the fingerprint file and then checking that fingerprint against the verification material.

How to verify a receipt

Use lattiq-verify to verify a receipt offline. The POST response body from /v1/observe is the receipt JSON — save it to a file (e.g., curl ... > receipt.json) before running the verifier.

The customer-facing recipe has three commands. Run them from a working directory of your choice; the commands fetch the trust-anchor file, clone the published merkle log, and run the verifier against the day your receipt was issued.

Verify recipe (3 commands)

curl -fsS https://botrite.lattiq.ai/keys/KEY_FINGERPRINTS.txt -o KEY_FINGERPRINTS.txt git clone https://github.com/Botrite/observe-merkle-log && cd observe-merkle-log lattiq-verify merkle --receipt /path/to/receipt.json --date 2026-04-29 --merkle-mirror . --key-fingerprints ../KEY_FINGERPRINTS.txt

A few practical notes:

  • Install the verifier with pip install lattiq-verify. On Windows or in environments where multiple Python interpreters are present, use python -m pip install lattiq-verify.
  • The --date argument is the UTC date prefix of the receipt’s observed_at field — the first 10 characters. For the example receipt above, observed_at is 2026-05-01T13:42:11Z, so --date is 2026-05-01.
  • The --merkle-mirror . argument means the verifier reads the published merkle data from your current working directory. You are inside the cloned observe-merkle-log directory after running the second command, which is exactly what the verifier expects.
  • The --key-fingerprints ../KEY_FINGERPRINTS.txt argument points back to the parent directory because that is where you saved the trust-anchor file before changing into the cloned repo.
  • v0.1 contract. lattiq-verify v0.1 is the supported verifier contract for this workflow. If your package manager currently resolves lattiq-verify to 0.0.1, see /verify for the current published version status.

After downloading KEY_FINGERPRINTS.txt, compute its SHA-256 locally and compare it to the hash published on /verify before you trust the file:

sha256sum KEY_FINGERPRINTS.txt

Trust anchor

KEY_FINGERPRINTS.txt is the public trust-anchor file for receipt verification. It lists the public key fingerprints that the verifier is allowed to trust for receipt signing and merkle-log signing. The file is published at https://botrite.lattiq.ai/keys/KEY_FINGERPRINTS.txt.

The file lives on botrite.lattiq.ai/keys/ so the trust anchor is published independently of the repository and merkle data it helps authenticate. That separation matters: a verifier should not have to trust the same surface both to provide evidence and to define which signing keys are trusted for that evidence.

Before you verify receipts, download KEY_FINGERPRINTS.txt, compute its SHA-256 locally, and compare it with the SHA-256 published on /verify. If those hashes do not match, stop and refresh the file before you continue.

What receipts prove

Successful offline verification proves all of the following:

Verified propertyWhat it means
Receipt signature validityThe receipt was signed by the private key corresponding to the public verification material selected by signing.key_id.
Receipt integrityThe signed receipt payload has not been altered since it was signed.
Trust-anchor matchThe verification key material matches the independently-published fingerprint file you supplied.
Merkle inclusionThe receipt is included in the published merkle day for the date you verified.
Published-root consistencyThe receipt’s leaf hash, the published leaf set, and the published daily root are consistent with each other.

What receipts do NOT prove

Verification does not prove any of the following:

Not provenWhy not
That the observed page was true, safe, accurate, or trustworthyThe receipt is descriptive evidence of an observation, not a content judgment.
That the page owner was authentic, authorized, or acting in good faithThe receipt records what was observed, not who ultimately controls the content.
That the observed content will stay the same after the observation timeThe receipt is tied to one observation time, not to future behavior.
That your local machine is uncompromisedLocal file replacement or process tampering on your system remains outside the receipt’s trust boundary.
That no trust-anchor or certificate compromise is possible anywhere on the networkThe trust model depends on the fingerprint file you fetched and the HTTPS surface you used to fetch it.
That a non-SUCCESS result contains a normal content comparisonError outcomes are signed outcomes, but some do not represent a full comparable fetch.

Trust model and v0.1 limitations

An HTTPS-served KEY_FINGERPRINTS.txt on a domain separate from the merkle-log repository is the v0.1 trust anchor. It is consistent with early-stage trust postures used by other public-software provenance surfaces. It is honest about what it does not yet defend against. The four limitations below are tracked as v0.2 hardening commitments — not as backlog wishes, but as named follow-up work.

1. Single-domain compromise of the trust-anchor host

If an attacker can serve arbitrary bytes from botrite.lattiq.ai, they can serve a forged KEY_FINGERPRINTS.txt and a matching forged hash on /verify. v0.2 mitigation: publish a second mirror of KEY_FINGERPRINTS.txt on an independent surface so the customer can cross-check the trust anchor against two independent publication points.

2. CA misissuance against the trust-anchor host

If a public certificate authority incorrectly issues a certificate for the trust-anchor origin, an attacker could impersonate the HTTPS endpoint. v0.2 mitigation: publish and enforce a CAA record for the trust-anchor domain and run Certificate Transparency monitoring with alerting.

3. MITM with a stolen certificate

A valid but compromised certificate plus a path-position attacker still defeats plain HTTPS bootstrapping. v0.2 mitigation: enable HSTS on the trust-anchor origin and add cert-pinning support in lattiq-verify v0.2.

4. A compromised local customer machine remains out of scope

If the local machine is malicious, local file replacement or process tampering can still defeat verification. The verifier closes a narrow set of avoidable local-file risks (it refuses symlinks and world-writable trust-anchor files, reads the file once, and emits the consumed file’s SHA-256 for audit), but it does not claim to defend against host compromise.

If you are doing security due-diligence on Botrite Observe and want to dig into these v0.2 commitments, see /.well-known/security.txt for current contact and policy information.

Brand-stack

The tool is lattiq-verify, the repo is Botrite/observe-merkle-log, both are operated by Botrite Operations LLC under Lattiq, LLC.

Errors and HTTP responses

Except for 200 OK, Botrite Observe returns errors in this shape:

Error envelope

{ "error": "Human-readable message", "code": "machine_readable_code" }

Clients should depend on the HTTP status code and the code field. The error field is for operator-readable context.

HTTP status reference

HTTP statusWhat happenedExample envelopeWhat you should do
200 OKThe observation succeeded and the response body is a receipt.Receipt body shown in the Response section.Store the receipt if you need to verify or audit it later.
400 Bad RequestThe request was syntactically wrong or missing a required request element.{"error":"Bad request","code":"invalid_request"}Check your JSON body, headers, and request structure, then retry.
401 UnauthorizedThe bearer token was missing, expired, or invalid.{"error":"Unauthorized","code":"unauthorized"}Provide a valid API key in the Authorization header.
402 Payment RequiredThe account cannot process the request because included usage or credits are exhausted.{"error":"Payment required","code":"payment_required"}Add capacity to the account or wait for your usage to reset, then retry.
403 ForbiddenThe API key is recognized but is not allowed to use the endpoint, such as a revoked key.{"error":"Forbidden","code":"forbidden"}Stop retrying with that key and contact support if you believe the restriction is incorrect.
422 Unprocessable EntityThe JSON body was understood, but a field such as url failed validation.{"error":"Validation error","code":"validation_error"}Correct the invalid field and retry.
429 Too Many RequestsThe request exceeded a rate limit.{"error":"Rate limit exceeded","code":"rate_limited"}Back off and retry later. Honor any retry guidance your client policy applies.
500 Internal Server ErrorThe server could not complete the request.{"error":"Internal server error","code":"internal_error"}Retry with backoff. If the problem continues, contact support and include the time of the request.

Headers and caching

For POST /v1/observe, the normative response behavior is:

HeaderValueMeaning
Content-Typeapplication/jsonThe response body is JSON.
Cache-Controlno-store, privateReceipts and error responses for this endpoint should not be stored by caches.

Receipt verification data is carried in the JSON body. Clients should not depend on undocumented response headers for signature verification, schema dispatch, or service identification.

Ready to verify a receipt?

Run the three-command recipe against a receipt you’ve already received from POST /v1/observe.