# Builder Integration Guide

URL: https://docs.nexart.io/docs/builder-integration-guide

End to end builder guide for the NexArt canonical node: choosing a flow, authenticating, selecting a protocol version, binding identity data, and handling.

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.

Flow

```
your system  ->  choose a flow  ->  POST a payload  ->  node returns certificateHash + signature
                                                            |
                                                            v
                                              anyone can verify it later, forever
```

Two non-negotiable rules to design around:

- One `executionId` → one `certificateHash`, forever. Re-submitting the same `executionId` with the same content replays the original certificate. Re-submitting it with different content is rejected (`409`). Pick a stable, unique `executionId` per real execution.
- The node fails hard. It never &quot;fixes&quot; or silently strips input. If a field is missing or malformed, you get a `4xx` with 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.

Minimal example - private, hash-only

```
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 Execution Identity Binding (node 0.23.1)

`POST /v1/cer/ai/certify` accepts an optional top-level `identity` object that binds an execution to a subject. There are exactly two accepted input shapes. Both are closed: unknown keys, or a mix of the two shapes, are rejected with `400 VALIDATION_ERROR`. The node never coerces.

### Shape A - canonical AiExecutionIdentity (recommended)

Canonical identity input

```
{
  "identity": {
    "provider": "https://accounts.google.com",
    "sub": "10897700123456789",
    "assertionHash": "sha256:<64 lowercase hex>",
    "emailHash": "sha256:<64 lowercase hex>",
    "verified": true
  }
}
```

| Field | Required | Rule |
| --- | --- | --- |
| provider | yes | Non-empty string. The IdP issuer. |
| sub | yes | Non-empty string. Stable per-user subject at that provider. |
| assertionHash | yes | String matching sha256: + 64 lowercase hex. A SHA-256 commitment to the raw identity assertion (e.g. the JWT), computed by the caller. The raw assertion is never sent. |
| emailHash | no | Same sha256:<64 lowercase hex> format. Never the raw email. |
| verified | yes | Literal boolean (true or false). Strings and numbers are rejected. |

Canonical input is sealed verbatim into `snapshot.identity`.

### Shape B - legacy self-asserted {type, value} (backward compatible)

Legacy identity input

```
{
  "identity": { "type": "email", "value": "user@company.com" }
}
```

Both fields must be non-empty strings. The node normalizes this into the canonical shape before sealing, deterministically:

Normalization

```
provider      = urn:nexart:self-asserted:<type>
sub           = sha256: + hex(SHA-256 of <type> + NUL byte (\u0000) + <value>)
assertionHash = sha256: + hex(SHA-256 of the canonical JSON (RFC 8785 / JCS) of {"type":"<type>","value":"<value>"})
verified      = false (always)
```

The raw `value` is discarded after normalization. It never appears in the CER, the certificate hash input, DB proof records, Project Bundles, resolver responses, logs, or validation error messages. The same input always produces the same sealed identity: re-certification of the same `executionId` and identity reproduces the same `certificateHash`.

### Sealed representation

Regardless of input shape, the sealed `snapshot.identity` is always the SDK&#x27;s canonical closed shape: `{provider, sub, assertionHash, emailHash?, verified}` - exactly the shape `@nexart/ai-execution@1.4.0` `verifyProjectBundle()` enforces. Identity-bound CERs therefore pass SDK Project Bundle verification. Identity rides on the snapshot and is bound into `certificateHash`, the signed envelope, and public canonical payloads.

### Assurance semantics of verified

- `verified` is a producer assertion, sealed verbatim. `verified: true` means the calling application attests that it externally verified the identity assertion (e.g. validated the IdP token via JWKS) before certifying. It does not mean NexArt verified the identity. The node performs no identity verification and is a passive signer.
- `verified: false` is a valid, truthful lower-assurance state, and is what legacy self-asserted input always seals.
- The node never upgrades `false` to `true` or downgrades `true` to `false`.

### Anonymous behavior

Omit `identity` entirely and no identity is sealed; the snapshot is byte-identical to pre-identity certificates. Unchanged from all previous versions.

### Backward compatibility

- The canonical normalization applies only to CERs issued from node `0.23.1` onward.
- Existing CERs are never migrated, rewritten, rehashed, or re-sealed. Historical `certificateHash` values and Project Hashes are immutable.
- Older CERs continue to verify under their original semantics: standalone verification, exact-hash lookup, public resolvers, receipts, envelopes, and RFC 3161 evidence are all unchanged.
- Project Bundle verification rules were not weakened. An identity-bound CER issued before `0.23.1` remains a valid historical CER but is not eligible for Project Bundle assembly, because it never satisfied that contract.

### Error behavior

Malformed `identity` input returns `400 VALIDATION_ERROR`. Error messages are PII-safe by design: they never echo supplied identity values or caller-supplied property names.

## 5.2 Binding Other PII into the Certificate (Where It Must Go)

For PII beyond the `identity` contract (name, customer id, free-form subject data), bind it through the sealed payload so the proof is provably about that subject without storing the PII in plaintext.

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 `certificateHash`and 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:

Binding subject data into the sealed input (1.3.1)

```
{
  "protocolVersion": "1.3.1",
  "provider": "openai",
  "model": "gpt-4o",
  "executionId": "exec-2026-06-29-user-7f3a",
  "prompt": "Generate the onboarding summary.",
  "input": "{\"subject\":{\"name\":\"Jane Doe\"},\"content\":\"<the actual model input>\"}",
  "output": "<the raw model output>",
  "parameters": { "temperature": 0.2, "maxTokens": 1024, "topP": 0.95, "seed": 42 }
}
```

Result: the subject data is sealed into the commitment, bound by `certificateHash`, and never stored or logged in plaintext. 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`/`output`and 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 |

Confidential example - protocolVersion 1.3.1

```
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`/`output` are sealed into commitment envelopes; only the envelopes and their hashes are certified and stored.
- The same `executionId` always produces the same commitment and `certificateHash` (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` / classification `VERIFIED`, content-mode `HASH_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):

200 OK response

```
{
  "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 a `draw()` and `totalFrames > 1`).
- `totalFrames` must be `2 ... MAX_TOTAL_FRAMES` (default `1800`, operator-configurable via the `MAX_TOTAL_FRAMES` env var); outside that → `400 LOOP_MODE_ERROR`.
- Loop mode requires a `draw()` function. If `draw()` 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-abc123
```

Returns 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.

POST /v1/project-bundle/register (authenticated)

```
{
  "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 same `certificateHash` back. Safe to retry on network failure.
- A conflicting retry (same `executionId`, changed content) is rejected with `409`. This is by design - a certified execution is immutable.
- For `1.3.1`, sealing salts are derived deterministically from `executionId`, so the commitment and `certificateHash` are stable across retries and redeploys.
- For fully reproducible hashes in standard mode, also send a fixed `createdAt` and `timestamp`; otherwise the node mints a fresh `createdAt` on 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 `executionId` for the execution.
- Shape the payload per sections 5 / 6 / 8 - respect required fields and the `input` XOR `inputHash` rule (and the `1.3.1` &quot;raw only, no hashes&quot; rule).
- Optional: preview with `POST /v1/cer/ai/create`.
- POST to the certify endpoint. On `200`, store `certificateHash` + `executionId` (and keep the `verificationEnvelope` / `signature` if you verify offline).
- Verify anytime with `POST /v1/cer/verify` or `GET /v1/cer/public`.

For the exhaustive wire contract see Attestation Node, Independent Verification, and Confidential Execution.
