Your first signed decision
in about fifteen minutes
Six steps against the live /v1 API. No SDK, nothing to install beyond curl and Node 18+. Every /v1 response is enveloped: {"success":true,"data":{…}} on success, {"success":false,"error":…,"message":…} on failure.
Create your tenant
~2 minOne call creates your tenant on the Trial plan, its first API key, and its Ed25519 signing key pair. The response data holds tenantId, apiKey, signingPubKey, and verifyEmailSent. Passwords need at least 10 characters.
curl -s -X POST 'https://www.writteninstone.io/v1/signup' \
-H 'Content-Type: application/json' \
-d '{"email":"you@company.com","password":"a-long-passphrase","company":"Acme"}'
Example data — for illustration.
apiKey (sk_live_…) is shown exactly once; only its hash is stored. Pin the signingPubKey — step 5 verifies against the copy you keep, not the one we send later.export STONE_KEY=sk_live_… · export SIGNING_PUBKEY=…Signup is rate-limited to 10 requests per minute by default. You can re-read the public key any time with GET /v1/tenants/pubkey.
Raise your first decree
~3 minSend a truth: any JSON facts about a decision in front of your systems, plus a domain. The model council deliberates and StoneAI returns a decree with status pending. StoneAI records the decision; it does not act in your systems.
curl -s -X POST 'https://www.writteninstone.io/v1/decrees' \
-H "Authorization: Bearer $STONE_KEY" \
-H 'Content-Type: application/json' \
-d '{"domain":"ops","truth":{"signal":"502 spike","target":"checkout-service"}}'
Example data — for illustration.
From data, keep id (export DECREE_ID=…) and required_scope. You will also see action, confidence, and content_hash — the SHA-256 seal your approval will be bound to. The creation is written to your tenant's hash-chained audit ledger.
- The domain sets the scope:
ops→PRODUCTION,money→PAYMENT,compliance→COMPLIANCE, anything else →GENERAL. - If no council seat answers, the decree comes back with action
hold. - The Trial plan includes 50 decrees a month; past that the call returns
402 quota_exceeded.
Approve it with the required scope
~2 minApprove (or /deny) by presenting the decree's required_scope. StoneAI signs the decision with your tenant's Ed25519 key. The signature covers the tenant, decree, approver, scope, content hash, a random nonce, an expiry, and the verdict itself.
curl -s -X POST "https://www.writteninstone.io/v1/decrees/$DECREE_ID/approve" \
-H "Authorization: Bearer $STONE_KEY" \
-H 'Content-Type: application/json' \
-d '{"approverId":"you@company.com","scope":"PRODUCTION"}'
Example data — for illustration.
Success returns {"status":"approved","decreeId":…}. A scope that doesn't match returns 403. A decree that already has a verdict returns 409 already_decided — it cannot be decided twice.
approverId is recorded as you send it; it is not yet tied to the authenticated user.Fetch the Covenant Receipt
~1 minA decided decree yields a receipt: the signed approval, the decree's content hash, your public key, the matching audit-ledger entry, and the newest Merkle anchor if one exists. Undecided decrees return 404.
curl -s "https://www.writteninstone.io/v1/decrees/$DECREE_ID/receipt" \
-H "Authorization: Bearer $STONE_KEY" \
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.stringify(JSON.parse(s).data,null,2)))' \
> receipt.json
Verify it on your own machine
~2 minSave this as verify.mjs. It uses only Node's built-in crypto and makes no network call. It checks the receipt against the public key you pinned in step 1.
// verify.mjs — Node 18+, zero dependencies
import { readFileSync } from 'node:fs';
import { createHash, createPublicKey, verify } from 'node:crypto';
const [file, pinnedKey] = process.argv.slice(2);
const r = JSON.parse(readFileSync(file, 'utf8'));
const a = r.approval;
const sha256 = (s) => createHash('sha256').update(s).digest();
const verdict = r.decision === 'approved' ? 'approve' : 'deny';
// The exact tuple StoneAI signs (v2 carries the verdict inside the signature).
const tuple = r.v === 2
? [2, a.tenantId, a.decreeId, a.approverId, a.scope, a.contentHash, a.nonce, a.expiry, a.decision]
: [a.tenantId, a.decreeId, a.approverId, a.scope, a.contentHash, a.nonce, a.expiry];
// Raw 32-byte Ed25519 key -> SPKI DER, so node:crypto can use it.
const key = createPublicKey({
key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), Buffer.from(pinnedKey, 'hex')]),
format: 'der', type: 'spki',
});
const checks = {
keyMatchesPinned: r.pubKey === pinnedKey,
signature: verify(null, sha256(JSON.stringify(tuple)), key, Buffer.from(a.signature, 'hex')),
boundToDecree: a.decreeId === r.decreeId && a.contentHash === r.contentHash,
decision: r.chain.eventType === 'decree.' + r.decision && (r.v !== 2 || a.decision === verdict),
auditEntry: r.chain.entryHash === sha256(JSON.stringify({
tenantId: r.tenantId, eventType: r.chain.eventType, payload: r.chain.payload, prev: r.chain.prevHash,
})).toString('hex'),
};
console.log(checks);
process.exit(Object.values(checks).every(Boolean) ? 0 : 1);
node verify.mjs receipt.json "$SIGNING_PUBKEY"
Every check prints true and the script exits 0. Change the verdict, the scope, the approver, or the content hash inside receipt.json and it fails.
/proof/<tenantId>/<decreeId>, which runs the same checks server-side.Receive decree events by webhook (optional)
~5 minRegister one HTTPS endpoint and a shared secret of at least 16 characters. The secret is stored encrypted and never returned.
curl -s -X PUT 'https://www.writteninstone.io/v1/webhook' \
-H "Authorization: Bearer $STONE_KEY" \
-H 'Content-Type: application/json' \
-d '{"url":"https://your-app.example/hooks/stoneai","secret":"replace-with-a-long-random-secret"}'
curl -s -X POST 'https://www.writteninstone.io/v1/webhook/test' \
-H "Authorization: Bearer $STONE_KEY"
Example data — for illustration.
- Events:
decree.created,decree.approved,decree.denied(all sent to the one endpoint), pluswebhook.testfrom the test call. - Body:
{"id","event","payload","ts"}.idstays the same across retries — use it to dedupe. - Signature: header
x-stoneai-signature: t=<unix>,v1=<hex>, wherev1is HMAC-SHA256 of<t>.<raw body>with your secret. Reject stale timestamps. - Delivery: 8-second timeout, retried after 30 seconds and again after 5 minutes. Every attempt is listed at
GET /v1/webhook/deliveries.
Every endpoint and schema: API reference · live health: status