# Builder Integration Guide

URL: https://docs.nexart.io/docs/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.

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 "fixes" 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 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 `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 identity 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": "{\"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`/`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` "raw only, no hashes" 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.
