Botrite Observe API
POST a URL. Get a signed Ed25519 receipt. Verify offline against a daily-published merkle log.
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
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
url | string | Yes | The public URL to observe. | https://example.com/article |
Request headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer token for your Botrite Observe API key. |
Content-Type | Yes | Send 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 name | Type | Description |
|---|---|---|
chain_state | string enum | Whether this observation is the first one for the URL, matches the prior content, differs from the prior content, or cannot be compared. |
content_hash | string | SHA-256 hash of the normalized content bytes observed for this receipt. |
disclaimer_hash | string | SHA-256 hash of the legal disclaimer text in force when the receipt was signed. |
fetch_policy.final_url | string | Final destination URL after redirects, in canonicalized form. |
fetch_policy.max_bytes | integer | Maximum number of bytes the observation fetch was allowed to read. |
fetch_policy.mode | string | Fetch mode used for the observation. |
fetch_policy.timeout_ms | integer | Maximum fetch time in milliseconds. |
normalization.method | string | Normalization method used before hashing the content. |
normalization.spec_hash | string | SHA-256 hash of the normalization rules document used for this receipt. |
normalization.version | string | Version label for the normalization rules used for this receipt. |
observation_profile_hash | string | SHA-256 hash binding the full observation methodology used for the receipt. |
observed_at | string | UTC timestamp when the observation was recorded. |
previous_receipt_id | string or null | The prior receipt in the observation chain for this URL, or null when there is no linked prior receipt. |
receipt_id | string | Unique identifier for this receipt. |
result | string enum | Outcome of the observation attempt. |
schema_version | string | Receipt schema version. |
url | string | The original URL you asked Botrite Observe to observe. |
signing.algorithm | string | Signature algorithm used for the receipt. |
signing.key_id | string | Opaque signing key identifier used to select the trust anchor during verification. |
signing.signature | string | Signature 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 value | Meaning |
|---|---|
SUCCESS | The URL was observed successfully and the receipt contains a normal content hash. |
ERROR | A general error prevented a normal observation outcome. |
TOO_LARGE | The response exceeded the allowed fetch size limit. |
UNREACHABLE | The URL could not be reached within the allowed network policy or timeout. |
BLOCKED_BY_ROBOTS | Access was denied by a robots policy. |
BLOCKED_BY_SITE | The site blocked the request directly. |
INVALID_RESPONSE | The response could be fetched but could not be interpreted as a valid observation input. |
PARTIAL_FETCH | Only part of the response could be fetched, so the observation was incomplete. |
NORMALIZATION_FAILED | The 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 value | Customer interpretation |
|---|---|
UNCHANGED | Observation recorded — content unchanged since the previous observation of this URL. |
CHANGED | Observation recorded — content changed since the previous observation. |
GENESIS | First observation of this URL. |
UNKNOWN | Observation 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, usepython -m pip install lattiq-verify. - The
--dateargument is the UTC date prefix of the receipt’sobserved_atfield — the first 10 characters. For the example receipt above,observed_atis2026-05-01T13:42:11Z, so--dateis2026-05-01. - The
--merkle-mirror .argument means the verifier reads the published merkle data from your current working directory. You are inside the clonedobserve-merkle-logdirectory after running the second command, which is exactly what the verifier expects. - The
--key-fingerprints ../KEY_FINGERPRINTS.txtargument 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-verifyv0.1 is the supported verifier contract for this workflow. If your package manager currently resolveslattiq-verifyto0.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 property | What it means |
|---|---|
| Receipt signature validity | The receipt was signed by the private key corresponding to the public verification material selected by signing.key_id. |
| Receipt integrity | The signed receipt payload has not been altered since it was signed. |
| Trust-anchor match | The verification key material matches the independently-published fingerprint file you supplied. |
| Merkle inclusion | The receipt is included in the published merkle day for the date you verified. |
| Published-root consistency | The 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 proven | Why not |
|---|---|
| That the observed page was true, safe, accurate, or trustworthy | The receipt is descriptive evidence of an observation, not a content judgment. |
| That the page owner was authentic, authorized, or acting in good faith | The receipt records what was observed, not who ultimately controls the content. |
| That the observed content will stay the same after the observation time | The receipt is tied to one observation time, not to future behavior. |
| That your local machine is uncompromised | Local 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 network | The 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 comparison | Error 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 status | What happened | Example envelope | What you should do |
|---|---|---|---|
200 OK | The 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 Request | The 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 Unauthorized | The bearer token was missing, expired, or invalid. | {"error":"Unauthorized","code":"unauthorized"} | Provide a valid API key in the Authorization header. |
402 Payment Required | The 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 Forbidden | The 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 Entity | The 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 Requests | The 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 Error | The 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:
| Header | Value | Meaning |
|---|---|---|
Content-Type | application/json | The response body is JSON. |
Cache-Control | no-store, private | Receipts 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.