nexart.iodocs

    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:

    1. 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.
    2. 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?

    SituationFlowEndpoint
    Ran an AI model and want to certify the callAI ExecutionPOST /v1/cer/ai/certify
    AI input/output is sensitive, must never be stored in plaintextAI 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 renderCode ModePOST /api/render, POST /verify
    Verify an existing certificateVerificationPOST /v1/cer/verify / GET /v1/cer/public
    Tying multiple certified steps into one projectProject BundlePOST /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>
    SituationResponse
    Header missing or not Bearer ...401 { "error": "UNAUTHORIZED" }
    Key invalid / inactive401 { "error": "UNAUTHORIZED" }
    Database temporarily unavailable503 { "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

    VersionWhen it appliesBehaviour
    (omitted) / "1.2.0"DefaultStandard 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 executionNode 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):

    FieldTypeRequiredNotes
    providerstringyese.g. "openai"
    modelstringyese.g. "gpt-4o"
    inputstringone of input / inputHashraw prompt/input text
    inputHashstringone of input / inputHashsha256:<hex>; submit this to keep input private
    outputstringone of output / outputHashraw model output
    outputHashstringone of output / outputHashsha256:<hex>
    executionIdstringrecommendedYour stable unique id. Auto-generated if omitted, but then you lose idempotency control.
    protocolVersionstringoptionalomit for 1.2.0
    parametersobjectoptionalmodel params (temperature, etc.)
    modelVersionstringoptional-
    appIdstringoptional-
    metadataobjectoptionalopaque, echoed back, not hashed
    contextobjectoptionalif present, must contain context.signals: [] (array)
    timestamp / createdAtISO-8601 stringoptionalsupply 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 itBound by certificateHash?Stored / visible?Use for PII?
    input / outputYes (via the sealed commitment)Never stored raw - confidentialYes - put PII here
    prompt, provider, model, parameters, executionId, contextYesPlaintext (stored and echoed)No
    metadataNo (outside the hash perimeter)PlaintextNo

    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:

    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/outputand the node hashes them for you. Submitting pre-computed hashes is rejected - the node must see the raw values to seal them.

    FieldTypeRequiredNotes
    protocolVersion"1.3.1"yesturns on confidential sealing
    providerstringyes-
    modelstringyes-
    inputstring (raw)yesnode seals it; never stored raw
    outputstring (raw)yesnode seals it; never stored raw
    promptstringyesnon-empty; stays plaintext (part of the certified record)
    parametersobjectyesmust be { temperature, maxTokens, topP, seed }
    parameters.temperaturenumberyesfinite number
    parameters.maxTokensnumberyesfinite number
    parameters.topPnumber | nullyesfinite number or null
    parameters.seednumber | nullyesfinite number or null
    inputHash / outputHash-forbiddensubmitting either → 400
    executionIdstringrecommendedstable 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

    HTTPerror / codeMeaningFix
    400VALIDATION_ERRORA field is missing/malformed; details[] lists each.Fix the offending field(s).
    400LOOP_MODE_ERRORtotalFrames out of 2..MAX_TOTAL_FRAMES (default 1800), or loop with no draw().Correct frames / add draw().
    401UNAUTHORIZEDMissing or invalid Bearer key.Send a valid Authorization: Bearer.
    409EXECUTION_MUTATION_DETECTEDSame executionId, different content.Use a new executionId, or resubmit identical content.
    422PROTOCOL_SCHEMA_UNSATISFIEDContent fails the requested protocol's SDK check.Match the protocol's required shape.
    500ATTESTATION_ERROR / INTERNAL_ERRORUnexpected server failure.Retry; contact operator if persistent.
    503SERVICE_UNAVAILABLEDB 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

    1. Obtain an API key; send it as Authorization: Bearer <key>.
    2. Pick your flow: AI standard, AI confidential 1.3.1, or Code Mode.
    3. Generate a stable, unique executionId for the execution.
    4. 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).
    5. Optional: preview with POST /v1/cer/ai/create.
    6. POST to the certify endpoint. On 200, store certificateHash + executionId (and keep the verificationEnvelope / signature if you verify offline).
    7. 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.