Builder Integration Guide
The practical 'get certified' reference. Which endpoint to call, how to shape the payload, and how to read the response.
This is the practical companion to the deeper protocol documentation. It mirrors the canonical builder integration guide shipped with the NexArt Canonical Node. For the full wire contract (envelope projection, signature pseudocode, key discovery, every endpoint), see Attestation Node and Independent Verification.
1. The 30-Second Mental Model
The node does one job: take a description of something that was executed (either creative code or an AI model call), run/seal it deterministically, and return a signed certificate that anyone can later verify offline.
your system -> choose a flow -> POST a payload -> node returns certificateHash + signature
|
v
anyone can verify it later, foreverTwo non-negotiable rules to design around:
- One
executionId→ onecertificateHash, forever. Re-submitting the sameexecutionIdwith the same content replays the original certificate. Re-submitting it with different content is rejected (409). Pick a stable, uniqueexecutionIdper real execution. - The node fails hard. It never "fixes" or silently strips input. If a field is missing or malformed, you get a
4xxwith a precise error - not a best-effort guess.
2. Which Flow Do I Need?
| Situation | Flow | Endpoint |
|---|---|---|
| Ran an AI model and want to certify the call | AI Execution | POST /v1/cer/ai/certify |
| AI input/output is sensitive, must never be stored in plaintext | AI Execution - Confidential (1.3.1) | POST /v1/cer/ai/certify with protocolVersion "1.3.1" |
| Ran creative code (p5.js) and want to certify the render | Code Mode | POST /api/render, POST /verify |
| Verify an existing certificate | Verification | POST /v1/cer/verify / GET /v1/cer/public |
| Tying multiple certified steps into one project | Project Bundle | POST /v1/project-bundle/register |
Decision tree for AI Execution. Is the input/output sensitive (PII, private prompts, customer data)?
- YES → use
protocolVersion: "1.3.1"(node seals raw I/O server-side). - NO → use the default (omit
protocolVersion→"1.2.0"); submit hashes or raw I/O.
3. Authentication
Every certifying endpoint is authenticated with a Bearer API key:
Authorization: Bearer <your_api_key>| Situation | Response |
|---|---|
Header missing or not Bearer ... | 401 { "error": "UNAUTHORIZED" } |
| Key invalid / inactive | 401 { "error": "UNAUTHORIZED" } |
| Database temporarily unavailable | 503 { "error": "SERVICE_UNAVAILABLE" } (fails closed) |
Public verification/lookup endpoints (POST /v1/cer/verify, GET /v1/cer/public, GET /api/public-cer-proof) need no API key. Admin endpoints (/v1/admin/*) use the X-Admin-Secret header and are not part of normal integration.
4. Protocol Versions Accepted by the Node
| Version | When it applies | Behaviour |
|---|---|---|
| (omitted) / "1.2.0" | Default | Standard hash-bound certificate. Byte-identical to legacy. |
| "1.3.0" | Transitional (still accepted) | Standard certificate, JCS (RFC 8785) canonicalization auto-selected. |
| "1.3.1" | Confidential AI execution | Node seals raw input/output into commitments server-side. |
The node currently accepts 1.2.0, 1.3.0, and 1.3.1. The recommended default for new confidential work is 1.3.1 (advertised by GET /). Any other value is rejected with 400 VALIDATION_ERROR.
All hashes are SHA-256 in the form sha256:<64-lowercase-hex> - never raw hex, never base64.
5. AI Execution - Standard Certification
POST /v1/cer/ai/create - preview (no signing, no storage)
Use this to see the bundle and its certificateHash before committing. It does not sign or persist anything. Same request shape as certify.
POST /v1/cer/ai/certify - sign and persist
This is the endpoint that produces a permanent, signed certificate. Required and optional fields for standard mode (1.2.0 / 1.3.0):
| Field | Type | Required | Notes |
|---|---|---|---|
| provider | string | yes | e.g. "openai" |
| model | string | yes | e.g. "gpt-4o" |
| input | string | one of input / inputHash | raw prompt/input text |
| inputHash | string | one of input / inputHash | sha256:<hex>; submit this to keep input private |
| output | string | one of output / outputHash | raw model output |
| outputHash | string | one of output / outputHash | sha256:<hex> |
| executionId | string | recommended | Your stable unique id. Auto-generated if omitted, but then you lose idempotency control. |
| protocolVersion | string | optional | omit for 1.2.0 |
| parameters | object | optional | model params (temperature, etc.) |
| modelVersion | string | optional | - |
| appId | string | optional | - |
| metadata | object | optional | opaque, echoed back, not hashed |
| context | object | optional | if present, must contain context.signals: [] (array) |
| timestamp / createdAt | ISO-8601 string | optional | supply for fully reproducible hashes on retry |
You MAY submit input OR inputHash, never both (same for output). Providing both returns 400. To keep content private, submit the hash form.
curl -X POST https://node.nexart.io/v1/cer/ai/certify \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "openai",
"model": "gpt-4o",
"executionId": "exec-2026-06-29-abc123",
"inputHash": "sha256:5f2c...e9",
"outputHash": "sha256:91a0...4d",
"parameters": { "temperature": 0.7 }
}'5.1 Binding Identity / PII into the Certificate (Where It Must Go)
A common requirement is to bind a person's identity (name, email, customer id, or any PII) to the certificate so the proof is provably about that subject - without storing the PII in plaintext.
The node builds the certified snapshot from a fixed set of fields. Any unknown top-level field you invent (e.g. identity) is silently dropped - it will NOT be bound by certificateHash, and it will NOT raise an error. That silent drop is the trap: it looks like it worked, but the binding is fake.
Here is exactly where each field lands under 1.3.1:
| Where you put it | Bound by certificateHash? | Stored / visible? | Use for PII? |
|---|---|---|---|
| input / output | Yes (via the sealed commitment) | Never stored raw - confidential | Yes - put PII here |
| prompt, provider, model, parameters, executionId, context | Yes | Plaintext (stored and echoed) | No |
| metadata | No (outside the hash perimeter) | Plaintext | No |
Rule of thumb: the only slot that is both bound by certificateHashand confidential is input / output. To bind PII, embed it inside input (or output) - typically as a JSON-encoded string so it stays structured and deterministic:
{
"protocolVersion": "1.3.1",
"provider": "openai",
"model": "gpt-4o",
"executionId": "exec-2026-06-29-user-7f3a",
"prompt": "Generate the onboarding summary.",
"input": "{\"identity\":{\"name\":\"Jane Doe\",\"email\":\"jane@example.com\"},\"content\":\"<the actual model input>\"}",
"output": "<the raw model output>",
"parameters": { "temperature": 0.2, "maxTokens": 1024, "topP": 0.95, "seed": 42 }
}Result: the identity is sealed into the commitment, bound by certificateHash, and never stored or logged in plaintext. There is no separate sealed identity slot in the 1.3.1 schema - input / output is the mechanism. Keep prompt non-empty and PII-free; it is bound by the hash but stored in plaintext.
Do NOT put PII in prompt, context, or metadata. prompt/context are bound by the hash but stored in plaintext; metadata is not bound at all, so it provides no real cryptographic binding.
6. AI Execution - Confidential Certification (1.3.1)
Use this when the raw input/output must never be stored, but you still want a verifiable certificate. The node seals your raw values into cryptographic commitments server-side; the raw text never enters the certificate, the signed envelope, the local ledger, persistent storage, or the logs. See Confidential Execution for the full model.
Key difference from standard mode: with 1.3.1 you submit raw input/outputand the node hashes them for you. Submitting pre-computed hashes is rejected - the node must see the raw values to seal them.
| Field | Type | Required | Notes |
|---|---|---|---|
| protocolVersion | "1.3.1" | yes | turns on confidential sealing |
| provider | string | yes | - |
| model | string | yes | - |
| input | string (raw) | yes | node seals it; never stored raw |
| output | string (raw) | yes | node seals it; never stored raw |
| prompt | string | yes | non-empty; stays plaintext (part of the certified record) |
| parameters | object | yes | must be { temperature, maxTokens, topP, seed } |
| parameters.temperature | number | yes | finite number |
| parameters.maxTokens | number | yes | finite number |
| parameters.topP | number | null | yes | finite number or null |
| parameters.seed | number | null | yes | finite number or null |
| inputHash / outputHash | - | forbidden | submitting either → 400 |
| executionId | string | recommended | stable id; drives deterministic sealing |
curl -X POST https://node.nexart.io/v1/cer/ai/certify \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"protocolVersion": "1.3.1",
"provider": "openai",
"model": "gpt-4o",
"executionId": "exec-2026-06-29-private-001",
"prompt": "Summarize the attached medical record.",
"input": "<<raw sensitive input text>>",
"output": "<<raw sensitive model output>>",
"parameters": { "temperature": 0.2, "maxTokens": 1024, "topP": 0.95, "seed": 42 }
}'What the node guarantees for 1.3.1:
- Raw
input/outputare sealed into commitment envelopes; only the envelopes and their hashes are certified and stored. - The same
executionIdalways produces the same commitment andcertificateHash(deterministic salts) - so retries are idempotent. - On any failure path, raw I/O is stripped before anything is logged or persisted.
- Verifying the result returns SDK verdict
VERIFIED_CONFIDENTIAL/ classificationVERIFIED, content-modeHASH_BOUND. - The Code-Mode render path explicitly rejects
1.3.1- there is no raw AI I/O to seal there.
7. The Certify Response
Both standard and confidential certify return the same envelope shape on success (HTTP 200):
{
"ok": true,
"cer": { /* the full CER bundle that was certified */ },
"certificateHash": "sha256:...",
"bundleType": "cer.ai.execution.v1",
"createdAt": "2026-06-29T12:00:00.000Z",
"attestationId": "...",
"nodeRuntimeHash": "sha256:...",
"protocolVersion": "1.3.1",
"issuedAt": "2026-06-29T12:00:00.100Z",
"kid": "...",
"attestorKeyId": "...",
"attestation": { /* attestation summary */ },
"receipt": { /* signed receipt fields */ },
"signature": "base64url...",
"verificationEnvelope": { /* envelope v2 */ },
"verificationEnvelopeSignature": "base64url...",
"timestamp": { "type": "rfc3161|node-issued", "status": "...", "provider": "...", "token": "..." },
"verificationUrl": "https://...",
"requestId": "..."
}The certificateHash is also returned as the response header X-Certificate-Hash. Persist certificateHash + executionId on your side. They are the keys you use for later lookups and the proof that your execution was certified.
8. Code Mode Certification (p5.js)
POST /api/render - render code to an image (authenticated)
{
"code": "function setup(){ createCanvas(1950,2400); } function draw(){ /* ... */ }",
"seed": "default",
"VAR": [0, 1, 2],
"width": 1950,
"height": 2400,
"protocolVersion": "1.2.0"
}The canvas size is fixed by the node. width/height, if sent, must equal 1950 x 2400 or you get 400. With Accept: application/json the response is:
{ "pngBase64": "...", "runtimeHash": "sha256:...", "width": 1950, "height": 2400,
"sdkVersion": "...", "protocolVersion": "...", "executionTimeMs": 123 }Without that header you receive the raw image/png plus an X-Runtime-Hash header. The public POST /render is disabled in production - use the authenticated POST /api/render.
POST /verify - certify a render or animation (authenticated, executes code)
{
"snapshot": {
"code": "...",
"seed": "default",
"vars": [0,1,2],
"execution": { "mode": "loop", "totalFrames": 120, "fps": 30 }
},
"expectedHash": "sha256:...",
"expectedAnimationHash": "sha256:...",
"expectedPosterHash": "sha256:..."
}Loop mode rules:
- Loop mode triggers when
execution.mode === "loop"(or the code defines adraw()andtotalFrames > 1). totalFramesmust be2 ... MAX_TOTAL_FRAMES(default1800, operator-configurable via theMAX_TOTAL_FRAMESenv var); outside that →400 LOOP_MODE_ERROR.- Loop mode requires a
draw()function. Ifdraw()is missing it FAILS - it never falls back to a static render.
9. Verifying a Certificate (no API key)
POST /v1/cer/verify - stateless verdict
// request
{ "bundle": { /* a complete CER bundle */ } }
// response
{
"status": "verified|failed",
"reasonCode": "OK|INPUT_HASH_MISMATCH|CERTIFICATE_HASH_MISMATCH|SIGNATURE_INVALID|...",
"checks": { "structure": "pass", "certificateHash": "pass", "inputHash": "pass",
"outputHash": "pass", "signature": "pass|skipped" },
"computedCertificateHash": "sha256:...",
"classification": "VERIFIED|LEGACY_VERIFIED|UNVERIFIABLE|FAILED",
"timestamp": { "type": "rfc3161|node-issued", "status": "verified|invalid|untrusted" }
}GET /v1/cer/public - look up a stored proof
Query by either key (parameters are snake_case):
GET /v1/cer/public?certificate_hash=sha256:...
GET /v1/cer/public?execution_id=exec-2026-06-29-abc123Returns the stored proof including a canonical block that lets an external verifier verify by direct hashing + direct Ed25519 verification - no bundle reconstruction needed. GET /api/public-cer-proof is the privacy-preserving variant. Both return 404 for records that have been hidden/deleted by retention.
10. Project Bundles (Multi-Step Executions)
Tie several already-certified steps into one project proof.
{
"bundleType": "cer.project.bundle.v1",
"integrity": { "projectHash": "sha256:..." },
"totalSteps": 5,
"stepRegistry": [ { "stepId": "step-1", "certificateHash": "sha256:..." } ],
"embeddedBundles": { "step-1": { /* the certified step bundle */ } }
}POST /v1/project-bundle/verify takes { "bundle": { ... } } and returns a per-step pass/fail summary plus a projectHash submitted-vs-computed check.
11. Error Catalogue
| HTTP | error / code | Meaning | Fix |
|---|---|---|---|
| 400 | VALIDATION_ERROR | A field is missing/malformed; details[] lists each. | Fix the offending field(s). |
| 400 | LOOP_MODE_ERROR | totalFrames out of 2..MAX_TOTAL_FRAMES (default 1800), or loop with no draw(). | Correct frames / add draw(). |
| 401 | UNAUTHORIZED | Missing or invalid Bearer key. | Send a valid Authorization: Bearer. |
| 409 | EXECUTION_MUTATION_DETECTED | Same executionId, different content. | Use a new executionId, or resubmit identical content. |
| 422 | PROTOCOL_SCHEMA_UNSATISFIED | Content fails the requested protocol's SDK check. | Match the protocol's required shape. |
| 500 | ATTESTATION_ERROR / INTERNAL_ERROR | Unexpected server failure. | Retry; contact operator if persistent. |
| 503 | SERVICE_UNAVAILABLE | DB unavailable (fails closed). | Retry later. |
Validation responses carry a human-readable message (errors joined by ;) and a machine-readable details array:
{
"error": "VALIDATION_ERROR",
"message": "protocolVersion \"1.3.1\" requires a non-empty prompt string; parameters.maxTokens must be a finite number",
"details": [
"protocolVersion \"1.3.1\" requires a non-empty prompt string",
"parameters.maxTokens must be a finite number"
],
"requestId": "..."
}12. Determinism and Idempotency
- Always send your own
executionId. It is the anchor for idempotency and lookups. - A true retry (same
executionId, same content) replays the original signed certificate - you get the samecertificateHashback. Safe to retry on network failure. - A conflicting retry (same
executionId, changed content) is rejected with409. This is by design - a certified execution is immutable. - For
1.3.1, sealing salts are derived deterministically fromexecutionId, so the commitment andcertificateHashare stable across retries and redeploys. - For fully reproducible hashes in standard mode, also send a fixed
createdAtandtimestamp; otherwise the node mints a freshcreatedAton first certify (still idempotent for true retries via replay).
13. A Complete "Get Certified" Checklist
- Obtain an API key; send it as
Authorization: Bearer <key>. - Pick your flow: AI standard, AI confidential
1.3.1, or Code Mode. - Generate a stable, unique
executionIdfor the execution. - Shape the payload per sections 5 / 6 / 8 - respect required fields and the
inputXORinputHashrule (and the1.3.1"raw only, no hashes" rule). - Optional: preview with
POST /v1/cer/ai/create. - POST to the certify endpoint. On
200, storecertificateHash+executionId(and keep theverificationEnvelope/signatureif you verify offline). - Verify anytime with
POST /v1/cer/verifyorGET /v1/cer/public.
For the exhaustive wire contract see Attestation Node, Independent Verification, and Confidential Execution.