Rox
Register · Developers
PUBLIC DOCS
v1 · api.roxregister.com
Sign in to Platform
OverviewQuickstartsGuidesSDKsAPI referenceErrorsWebhooksJWS vectors
Guides / Your first seal
Your first seal
From an empty Platform to a record anyone on earth can verify. The model in one sentence: you hash the document in your own systems, Rox seals the digest into an anchor on RoxChain, and the document itself never leaves you. A seal request is validated and either rejected immediately or accepted with 202; every accepted record then reaches exactly one terminal outcome, anchored with proof or failed.
1 · Set up in your Platform
Everything credential-shaped is created inside your Platform by your team; there is no public signup for keys. You need three things before the first API call:
  • A vault: the isolation boundary your records live in.
  • A record type (for example policy): it fixes the metadata schema and the publicly visible verify fields.
  • A vault API key with the seal scope, and a request-signing key: generate the keypair below and register the PUBLIC half under a key id (kid) of your choice. Additional keys can later be registered over the API, signed by an existing key.
shell
# Generate an Ed25519 request-signing keypair. The PRIVATE half stays
# with you; only the PUBLIC half is ever registered with Rox.
openssl genpkey -algorithm ed25519 -out request-key.pem
openssl pkey -in request-key.pem -pubout -out request-key.pub.pem

# Prefer JWK files for the SDKs:
node -e '
const { generateKeyPairSync } = require("node:crypto");
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
console.log(JSON.stringify({
  public_jwk: publicKey.export({ format: "jwk" }),
  private_jwk: privateKey.export({ format: "jwk" }),
}, null, 2));'
2 · Hash locally, then seal
Compute the SHA-256 of the exact bytes you want to be able to prove later, and send the digest to POST /v1/seal. The request carries your API key plus a detached JWS signed with your request-signing key, so possession of the API key alone can never anchor in your name. Send an Idempotency-Key so a network retry can never create a duplicate record. The signing rules, exactly:
  • Every mutation must carry the JWS; an unsigned mutation is refused with RR-1101.
  • Reads are authorized by the API key alone, the JWS is optional there. But a presented signature is never ignored: a signed read is validated exactly like a mutation, so a retired or unknown kid gets RR-1101 on any endpoint it is presented to. A passing signed read is therefore proof that the kid is still registered and active, and a signed read is the simplest way to verify a key rotation or retirement took effect.
  • Keys are vault-scoped, on both sides: an API key belongs to one vault, and the kid you sign with must be registered on that same vault. One kid cannot serve two vaults; give each vault its own request-signing key.
# Signing input (spec 7.3): RRv1\nPOST\n/v1/seal\nsha256(body)\nts\nnonce
DIGEST=$(shasum -a 256 policy.pdf | cut -d' ' -f1)
BODY='{"record_type":"policy","external_ref":"POL-2026-88123","content_digest_hex":"'"$DIGEST"'"}'
TS=$(($(date +%s) * 1000))                    # unix MILLISECONDS
NONCE=$(uuidgen | tr '[:upper:]' '[:lower:]') # single-use uuid
BODY_HASH=$(printf '%s' "$BODY" | shasum -a 256 | cut -d' ' -f1)
printf 'RRv1\nPOST\n/v1/seal\n%s\n%s\n%s' "$BODY_HASH" "$TS" "$NONCE" > /tmp/rr-input

# Raw Ed25519 signature over the canonical input, base64url, no padding:
SIG=$(openssl pkeyutl -sign -inkey request-key.pem -rawin -in /tmp/rr-input \
  | basenc --base64url -w0 | tr -d '=')

curl -sS -X POST https://api.roxregister.com/v1/seal \
  -H "Authorization: Bearer $RR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-RR-Key-Id: ish-prod-2026-01" \
  -H "X-RR-Timestamp: $TS" \
  -H "X-RR-Nonce: $NONCE" \
  -H "X-RR-Signature: $SIG" \
  -d "$BODY"
# → 202 {"record_id":"rec_…","reference":"rr_ref_01J8ZKQ2…","salt_hex":"…",
#        "status":"pending"}
# Accepted. Exactly one terminal outcome follows asynchronously:
# record.anchored (webhook) or record.failed. STORE salt_hex.
3 · Store what the 202 gives you
The acceptance body carries three values with different jobs:
  • record_id: your API handle for the record.
  • reference (rr_ref_...): public, safe to print on the document and in QR codes.
  • salt_hex: returned once, never again. Store it alongside your document; byte-exact verification needs it, and it is what keeps enumerable documents unguessable from the public record. It is never public and never on-chain.
4 · Wait for the terminal outcome
The 202 means validated and accepted, not anchored. Exactly one terminal outcome follows asynchronously: anchored (with anchor coordinates and proof) or failed. Consume it push-style via the record.anchored / record.failed webhooks (see the webhooks guide), or poll the record. Build on the outcome, not on any expectation of when it lands; the acceptance carries no timing fields, and anchoring scheduling is internal to Rox.
curl
curl -sS https://api.roxregister.com/v1/records/rr_ref_<ULID> \
  -H "Authorization: Bearer $RR_API_KEY"
# status: "pending" or "batched"  -> pre-anchor, treat identically, keep waiting
# status: "anchored"              -> terminal: anchor tx, slot and block time set
# status: "failed"                -> terminal: permanent failure after retries
5 · Verify like a stranger would
Verification is public by design: no key, no login, and the response never contains the salt or the digest. A reference lookup proves the record exists and is anchored; the digest + salt check proves the exact bytes you hold are the sealed bytes. People with a verify link or stamped QR use registry3.pebe.io; machines call the verify API directly.
# Status check by reference, public, no API key, no salt needed:
curl -sS https://api.roxregister.com/verify/rr_ref_01J8ZKQ2M3TT9AHW5RC2E9KQ2A
# → {"result":"genuine","record_type":"policy",
#    "issuer":{"corporate_name":"…","vault_name":"…"},
#    "anchored":{"tx":"3B4YxyQGKr…","slot":18230441,"block_time":"…"},
#    "status":"anchored","lifecycle":[…],"verify_fields":{…}}

# Byte-exact check of the file you hold (needs the record's salt_hex,
# which /v1/seal returned when you sealed it):
DIGEST=$(shasum -a 256 policy.pdf | cut -d' ' -f1)
curl -sS -X POST https://api.roxregister.com/verify/hash \
  -H "Content-Type: application/json" \
  -d '{"content_digest_hex":"'"$DIGEST"'",
       "salt_hex":"'"$RR_SALT_HEX"'",
       "reference":"rr_ref_01J8ZKQ2M3TT9AHW5RC2E9KQ2A"}'
# → same shape plus "match": true|false, one changed byte flips it
#   to false and result to "mismatch" (counted as a tamper signal)
Next: subscribe to webhooks so outcomes push to you, put the proof on the document itself with stamping, or commit per-field hashes with selective disclosure so you can later prove single fields without revealing the document. The SDKs wrap every call on this page.