Box
API reference Built
Two endpoints do the work: one to send events, one to collect the receipt. Everything else on this page is detail about those two.
Authentication
Every request carries two headers. The access id names the key and the secret proves it. Never put either in a URL, where it would land in logs and browser history.
x-entrail-access-id: acc_...
x-entrail-secret: <the secret>A bad id and a bad secret give the same 401 unauthorized, so nothing tells an attacker which half they guessed. Keys belong to one environment; where they come from is in key lifecycle.
POST /v1/events
| Answer | Means |
|---|---|
| 202 | Accepted and written to disk. Not yet sealed; that follows within seal_eta_seconds. |
| 400 | Unparseable body, or a body hash you supplied that does not match the bytes that arrived. |
| 401 | Bad access id or secret. It never says which. |
| 409 | A client id reused with a different body. Same id, same body is a duplicate and is fine. |
| 413 | Over the size limit: 256 KiB an event, 5 MiB or 1000 events a batch. |
| 422 | Failed the field contract. The reason names the field, never the value. |
| 503 | Not accepting right now. Retry with backoff. Nothing was stored. |
The response to a 202 carries payload_sha256, our hash of the exact bytes you sent. Compute it yourself and compare. If they ever differ, something altered your bytes in transit and you learn that without asking us. Both clients below do this comparison.
GET /v1/events/:id/receipt
Returns 404 until the event is sealed, so poll every few seconds. There is no deadline: a receipt you did not collect at the time is still collectable later. What to do with one is in verify a receipt.
A working client
Both send one event and compare the box's hash with their own. Python needs only the standard library (3.8 or newer); Node uses built-in fetch and crypto (18 or newer). Neither installs anything.
#!/usr/bin/env python3
"""Send one event to an Evidence Box and check the box's hash against your own.
Standard library only - no pip install. Python 3.8+.
export ENTRAIL_HOST=your-box.box.entrail.io
export ENTRAIL_ACCESS_ID=acc_...
export ENTRAIL_SECRET=... # keep it out of your shell history
python3 send_event.py
This file is executed before it is published. If it is on the docs site, it ran.
"""
import hashlib
import json
import os
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone
HOST = os.environ.get("ENTRAIL_HOST", "your-box.box.entrail.io")
ACCESS_ID = os.environ.get("ENTRAIL_ACCESS_ID", "")
SECRET = os.environ.get("ENTRAIL_SECRET", "")
event = {
"action": "record.update",
"actor.type": "user",
"actor.id": "u_1043",
"outcome": "success",
"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"event.description": "changed the amount on invoice 88123",
}
# Hash EXACTLY the bytes you are about to send. Serialise once, send that, hash that.
# Re-serialising for the hash is the usual way people end up with two different answers.
body = json.dumps(event, separators=(",", ":")).encode("utf-8")
mine = hashlib.sha256(body).hexdigest()
request = urllib.request.Request(
f"https://{HOST}/v1/events",
data=body,
method="POST",
headers={
"content-type": "application/json",
"x-entrail-access-id": ACCESS_ID,
"x-entrail-secret": SECRET,
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
status = response.status
answer = json.loads(response.read())
except urllib.error.HTTPError as err: # 4xx and 5xx arrive here
status = err.code
answer = json.loads(err.read() or b"{}")
print(f"HTTP {status}")
print(json.dumps(answer, indent=2))
if status != 202:
# 401 says nothing about which half was wrong, on purpose. 422 names the field, never the value.
print(f"\nnot accepted: {answer.get('error', answer)}", file=sys.stderr)
sys.exit(1)
theirs = answer["payload_sha256"]
print(f"\nmy hash {mine}")
print(f"their hash {theirs}")
print("MATCH - the bytes sealed are the bytes you sent" if mine == theirs
else "MISMATCH - something altered your bytes in transit")
sys.exit(0 if mine == theirs else 2)Hashing: serialise once
Serialise the event once, send those bytes, and hash those same bytes. If you build the request from an object and then hash a second serialisation of it, the two hashes will differ and nothing is actually wrong. Key order and whitespace are part of what gets sealed.
401 unauthorized and exit non-zero. Against a local server that hashes the request bytes the way this contract specifies, both print MATCH and exit zero. The end-to-end run against a real key, including the sealed receipt, is the demo we do in a pilot.