Rox
Register · Developers
PUBLIC DOCS
v1 · api.roxregister.com
Sign in to Platform
OverviewQuickstartsGuidesSDKsAPI referenceErrorsWebhooksJWS vectors
Node.js SDK
@rox-register/sdk
TypeScript-first client for Node 20+. Ships ESM and CommonJS builds with full type definitions and zero runtime dependencies. Request signing, local hashing, contract-respecting retries, selective disclosure and webhook verification are built in. You need a vault API key and a registered request-signing key, both created in your Platform.
Install
The package has no runtime dependencies; signing uses node:crypto.
shell
npm install @rox-register/sdk
# pnpm add @rox-register/sdk · yarn add @rox-register/sdk
# Node >= 20, ESM and CommonJS, TypeScript types included, zero dependencies
Seal, then verify
The complete happy path: construct the client, seal a document digest, observe the terminal outcome, verify the bytes. 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, delivered by webhook or polling. There are no anchoring options and no timing promises.
Node
import { readFileSync } from "node:fs";
import { RoxRegister } from "@rox-register/sdk";

const client = new RoxRegister({
  apiKey: process.env.RR_API_KEY,          // rr_live_ak_... (vault API key)
  requestSigningKey: {
    kid: "ish-prod-2026-01",               // registered in your Platform
    privateJwk: JSON.parse(readFileSync("request-key.jwk", "utf8")),
    // or privateKeyPem; Ed25519 and P-256 keys both supported
  },
});

// 1 · Seal. The SDK hashes the bytes locally; the file never leaves
//     your process. The request is validated and either rejected
//     immediately or accepted with 202.
const pdf = readFileSync("policy.pdf");
const accepted = await client.seal({
  recordType: "policy",
  externalRef: "POL-2026-88123",
  content: pdf,                            // or contentDigestHex: "..."
  idempotencyKey: "POL-2026-88123-v1",     // safe retries for 48h
});
// accepted.reference -> "rr_ref_01J8ZKQ2..."
// accepted.salt_hex  -> STORE IT: verification by digest needs it

// 2 · Exactly one terminal outcome follows asynchronously, anchored or
//     failed, via the record.anchored / record.failed webhooks, or poll:
const record = await client.getRecord(accepted.record_id);
// record.status: pending and batched are pre-anchor states, treat them
// identically; anchored and failed are the terminal outcomes.

// 3 · Anyone can verify the exact bytes, no API key needed:
const check = await client.verifyFile(pdf, {
  saltHex: accepted.salt_hex,
  reference: accepted.reference,
});
// check.match === true and check.result === "genuine", or one changed
// byte flips it to "mismatch" (counted, alerts the vault owner)
Consume webhooks
constructWebhookEvent verifies the HMAC signature constant-time, enforces the freshness window and returns the typed event envelope, or throws WebhookVerificationError. Full delivery contract on the webhooks page; end-to-end registration in the webhooks guide.
Node
import { constructWebhookEvent, WebhookVerificationError } from "@rox-register/sdk";
import express from "express";

const app = express();
// The MAC covers the EXACT raw body bytes: capture them before parsing.
app.post("/rox/events", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = constructWebhookEvent({
      secret: process.env.RR_WEBHOOK_SECRET,  // whsec_..., shown once at creation
      headers: req.headers,
      rawBody: req.body,                      // Buffer, untouched
    });
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.status(401).end();
    throw err;
  }
  // event: {id, type, created_at, data}, signature and freshness verified.
  if (event.type === "record.anchored") markProven(event.data);
  if (event.type === "record.failed") escalate(event.data);
  if (event.type === "verification.mismatch") alertSecurity(event.data);
  res.status(200).end();  // non-2xx deliveries are retried, then marked dead
});
Selective disclosure
Commit salted hashes of individual fields at sealing time, then reveal any subset later with inclusion proofs. Values and field salts never reach Rox. The full walkthrough is the selective disclosure guide.
Node
import { hashFields, revealPayload, verifyRevealedField } from "@rox-register/sdk";

// Sealing side: hash the fields locally. Values and per-field salts
// NEVER reach Rox, only the salted hashes do.
const { fieldHashes, fieldSalts, fieldRootHex } = hashFields([
  { name: "policy_no", value: "POL-2026-88123" },
  { name: "insured_name", value: "Aya Hassan" },
  { name: "coverage_egp", value: 250000 },
]);
await client.seal({ recordType: "policy", content: pdf, fieldHashes });
// Retain fieldSalts (and the values) alongside your document.

// Disclosure side: reveal ONLY chosen fields, with inclusion proofs.
const payload = revealPayload({
  fields,                      // the complete ordered list from sealing time
  fieldSalts,
  reveal: ["coverage_egp"],    // any subset
});

// Verifier side: check a revealed field against the anchored field root
// (field_root_hex from the issuer's record view or the RRP-1 proof
// bundle), fully offline.
const ok = verifyRevealedField(payload.revealed[0], record.field_root_hex);
Error handling
Every non-2xx response raises a typed subclass of RoxRegisterError carrying the RFC 7807 problem document and its stable RR-code. 429 responses are retried automatically honoring Retry-After; 5xx and network errors are retried only when the call is safe to repeat.
Node
import {
  RoxRegisterAuthError,
  RoxRegisterServerError,
  RoxRegisterStateError,
  RoxRegisterValidationError,
} from "@rox-register/sdk";

try {
  await client.seal({ recordType: "policy", contentDigestHex: digest });
} catch (err) {
  if (err instanceof RoxRegisterAuthError) {
    // RR-1xxx: API key, scope, JWS, timestamp, nonce
    rotateCredentialsAlert(err.code);
  } else if (err instanceof RoxRegisterValidationError) {
    // RR-2xxx: your request (vault state, record type, PII, idempotency)
    fixRequest(err.code, err.problem.detail);
  } else if (err instanceof RoxRegisterStateError) {
    // RR-3xxx / RR-4xxx: gas, backlog, approvals
    topUpGasOrRequestApproval(err.code);
  } else if (err instanceof RoxRegisterServerError) {
    // RR-5xxx: opaque internal; retry with backoff
    scheduleRetry(err.problem.instance);
  }
  // Every error carries the RFC 7807 problem document:
  // err.code, err.status, err.problem (type, title, detail, instance).
}
Client surface
Method names mirror the API reference; reads need only the API key, mutations additionally require the request-signing key configured on the client. Reads are sent unsigned by design (the API key authorizes them); a signature on a read is optional but validated when presented, so a signed read doubles as a live check that your kid is still active.
Sealing
seal(params)POST /v1/seal. Accepts contentDigestHex or raw content (hashed locally). Returns the 202 acceptance including salt_hex.
sealStamp(params)POST /v1/seal/stamp. Uploads a PDF, gets the stamped copy sealed: the SDK builds the multipart body as one buffer and signs the exact wire bytes. Acceptance adds content_digest_hex (of the STAMPED bytes) and the short-lived stamped_file_url.
sealBatch(items)POST /v1/seal/batch, up to 1,000 items under one JWS. Per-item results in request order; per-item idempotencyKey.
Records
getRecord(idOrReference)GET /v1/records/{id}. Status, digest, salt, leaf hash and, once anchored, the anchored object (chain tx, slot, block time, cluster), the same shape the public verify plane serves.
listRecords(filters)GET /v1/records, newest first, cursor pagination with status / type / ref / date filters.
iterateRecords(filters)Async generator that follows next_cursor for you.
Sign envelopes (sign scope)
createEnvelope(params)POST /v1/envelopes. Signed multipart create-and-send: source PDF plus signers and field placements; the envelope lands in status sent. Requires the sign_ses entitlement (RR-3101) and holds the service price in ROX (RR-3102 names the shortfall); the hold is captured only on completion.
getEnvelope(id)GET /v1/envelopes/{id}. Signers with signing_url, field layout, last 100 events, and, once assembled, the expiring final_file_url / certificate_file_url downloads plus the sealed record ids.
listEnvelopes(filters)GET /v1/envelopes, newest first, cursor pagination with an optional status filter.
voidEnvelope(id, { reason })POST /v1/envelopes/{id}/void. Withdraws a sent envelope; the hold is released and nothing is charged.
assembleEnvelope(id)POST /v1/envelopes/{id}/assemble. Idempotent assembly recovery after an assembly_failed event; never captures the fee twice.
Verify plane (public, no key)
verifyReference(reference)GET /verify/{reference}. Result, issuer names, anchor coordinates, lifecycle.
verifyHash({...})POST /verify/hash with content digest + record salt. Byte-exact match check.
verifyFile(content, {...})Hashes the bytes locally, then verifyHash. The content never leaves the process.
Vault and keys
getVault()GET /v1/vault, the calling key's vault profile: identity, status, environment, current authority key.
createVault(params)POST /v1/vaults (privileged, vaults.manage scope). Creates a vault, queues its DKG and returns a one-time API key for it. The vaults.manage scope is deliberately narrow: it grants vault creation ONLY, so a vaults.manage key is refused reads like getVault with RR-1005; that refusal is scoping, not a key problem. Use a read-scoped key for reads.
keyHistory()GET /v1/vault/key-history, authority key history for proof consumers, oldest first.
registerRequestKey(vaultId, {...})POST /v1/vaults/{id}/request-keys. Signed by an EXISTING active key.
retireRequestKey(vaultId, kid, {approvalId})DELETE /v1/vaults/{id}/request-keys/{kid}. Approval-gated.
Gas station
gasWallets()GET /v1/gas/wallets: Rox coin balances, caps and burn forecasts.
gasLedger(walletId, page)GET /v1/gas/wallets/{id}/ledger, deductions and deposits, newest first.
Billing reads (billing.read scope)
getGas()GET /v1/gas: the ROX deposit wallet (address, balance in lamports and as a ROX string) and the RoxChain cluster. Requires the billing.read scope, granted explicitly on the key; seal or read keys do not gain it implicitly (RR-1005 otherwise).
getGasLedger(page)GET /v1/gas/ledger: cursor-paginated cost ledger, newest first, with exact lamports and ROX string amounts plus the anchor tx signature each entry paid for.
Webhooks
createWebhook({url, events, vaultId?})POST /v1/webhooks. The returned secret is shown exactly once.
listWebhooks(page)GET /v1/webhooks, newest first.
webhookDeliveries(webhookId, page)GET /v1/webhooks/{id}/deliveries, the delivery log.
deleteWebhook(webhookId)DELETE /v1/webhooks/{id}, removes the endpoint and its log.
System
status()GET /v1/status: API health, anchoring backlog gauge, chain health, sandbox flag. Public.
Helpers (no network)
RoxRegister.hashContent(bytes) / sha256HexSHA-256 content digest as lowercase hex.
hashFields / revealPayload / verifyRevealedFieldSelective disclosure: salted field hashes, reveal payloads, local proof checks.
verifyWebhookSignature / constructWebhookEventConstant-time HMAC check (+ freshness) and typed event parsing.
RequestSigner / canonicalSigningInputThe RRv1 detached JWS, usable standalone with your own HTTP stack.