Neo4j logoAPI

The Neo4j Integration API, Route by Route

Ten routes, two surfaces, one rule about who is allowed to write. That is the core HTTP contract for governing a Neo4j estate with CoreModels, and this article documents it exhaustively — payloads, roles, response shapes, and the failure modes you will actually hit.

The Neo4j Integration API, Route by Route

Ten routes, two surfaces, one rule about who is allowed to write. That is the core HTTP contract for governing a Neo4j estate with CoreModels, and this article documents it exhaustively — payloads, roles, response shapes, and the failure modes you will actually hit.

Start with the map:

RouteSurfaceRoleWrites?
GET graph/integrations/vendorsinteractiveany authenticated userno
POST graph/integrations/neo4j/import/{projectId}interactiveAdminyes, additively
POST graph/integrations/neo4j/audit/{projectId}interactiveViewerno (opt-in history record)
POST graph/integrations/neo4j/reaudit/{projectId}interactiveViewerhistory record only
GET graph/integrations/neo4j/history/{projectId}interactiveViewerno
GET graph/integrations/neo4j/badge/{projectId}interactiveViewerno
POST graph/integrations/neo4j/generate/{projectId}interactiveViewerno
GET graph/integrations/neo4j/status/{projectId}interactiveViewerno
POST v1/{projectId}/integrations/neo4j/auditmachine-to-machineViewerno (opt-in history record)
GET v1/{projectId}/integrations/neo4j/badgemachine-to-machineViewerno

The interactive surface (graph/integrations/...) authenticates with your normal CoreModels login token. The v1 surface accepts revocable user API keys, which is why automation lives there. Every route is authenticated, and each one additionally enforces a per-project role — a token that can read a project cannot import into it.

Below, the host is https://coremodels.example.com, $TOKEN is the bearer credential for whichever surface you are calling, and $PROJECT_ID is a 32-character hex project id.

Discovery — the artifact contract, from the server

curl -sS -H "Authorization: Bearer $TOKEN" \
  "https://coremodels.example.com/graph/integrations/vendors" | jq '.vendors[] | select(.key == "neo4j")'
{
  "key": "neo4j",
  "displayName": "Neo4j",
  "capabilities": "Import, Audit, Generate",
  "artifacts": {
    "meta_schema": "required — the value returned by CALL apoc.meta.schema() (one JSON object)",
    "constraints": "optional — SHOW CONSTRAINTS rows as JSON (merges uniqueness/existence into the fields)"
  }
}

Client code should read this rather than hard-coding artifact names. The capabilities string is authoritative: Neo4j declares Import, Audit, and Generate, so every verb below is live for this vendor. (A fourth flag, live sync, exists in the model and is deliberately not implemented — CoreModels holds no database credentials.)

Import — POST graph/integrations/neo4j/import/{projectId} (Admin)

The request body is the shared artifacts shape: artifacts maps names to raw content strings, and the optional spaces array targets specific space ids (omit it for the project's main space).

curl -sS -X POST \
  "https://coremodels.example.com/graph/integrations/neo4j/import/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
        "artifacts": {
          "meta_schema": "{\"Person\":{\"type\":\"node\",\"count\":1200,\"properties\":{}}}",
          "constraints": "[]"
        },
        "spaces": []
      }'

The response is a counter block plus two honesty channels:

{ "success": true, "vendor": "neo4j", "projectName": "neo4j",
  "datasetsAdded": 3, "datasetsSkippedExisting": 0, "fieldsAdded": 0,
  "lineageEdgesAdded": 0, "lineageEdgesSkipped": 0, "nodesEnriched": 13,
  "snapshotStored": true, "lossiness": [], "errors": [] }

Three of these have semantics worth pinning down precisely. datasetsSkippedExisting counts labels already governed — import never mutates them, so re-running is safe and disagreements are the audit's job. fieldsAdded counts fields added to already-governed labels, the additive re-import path; a first import reports 0 because fresh labels arrive complete through the schema writer. snapshotStored reports whether the parsed estate snapshot was persisted; when the encoded snapshot exceeds the storage cap of roughly 1.5 million characters, it comes back false with an explanatory lossiness record, and re-audit is unavailable for that estate until the estate shrinks.

projectName is neo4j for this connector: a Neo4j instance has no project-name concept of its own, so one estate per governing project is the model.

Audit — POST graph/integrations/neo4j/audit/{projectId} (Viewer)

Same body, plus one flag. recordHistory defaults to false: the audit verb stays strictly read-only unless you ask for the run to be recorded.

curl -sS -X POST \
  "https://coremodels.example.com/graph/integrations/neo4j/audit/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"artifacts": {"meta_schema": "{\"Person\":{\"type\":\"node\",\"properties\":{}}}"}, "recordHistory": true}'

The full response shape:

{
  "success": true,
  "vendor": "neo4j",
  "projectName": "neo4j",
  "errorCount": 0,             // the CI gate: > 0 ⇒ governed meaning is violated
  "warningCount": 1,
  "infoCount": 1,
  "codes": { "label-no-unique-id": 1, "polymorphic-relationship": 1 },
  "driftedObjects": [],        // distinct subjects of Drift-section findings
  "fingerprint": "3f9a1c0d47b28e65",   // 16 hex chars, derived from the artifact content
  "metrics": {
    "Datasets (estate)": "3",
    "Datasets governed": "3 / 3",
    "Fields governed": "10 / 10",
    "Governed nodes with canonical mappings": "0 / 13 (0%)",
    "Last import": "2026-08-04T09:12:44.1180000+00:00"
  },
  "findings": [
    { "section": "Conformance", "severity": "Warning", "code": "label-no-unique-id",
      "subject": "Asset",
      "message": "Node label has no uniqueness constraint — MERGE-based ingestion can silently create duplicates.",
      "detail": null }
  ],
  "markdown": "# neo4j Schema Audit — neo4j\n\n🟡 **0 errors · 1 warnings · 1 info**\n…",
  "historyRecorded": true,
  "lossiness": []
}

section is one of Coverage, Drift, Conformance; severity is one of Error, Warning, Info. The fingerprint is a stable hash of the submitted artifact content, so two audits of byte-identical exports carry the same value — which is what makes "did anything actually change?" answerable from the history alone.

Reaudit — POST graph/integrations/neo4j/reaudit/{projectId} (Viewer)

The audit asks whether fresh artifacts still conform to the governed model. Reaudit asks the mirror-image question: the governed model moved — does the last-known estate still conform? It replays the same audit engine over the snapshot stored at import time, needs no artifacts, and unlike the audit verb it always records its run.

curl -sS -X POST \
  "https://coremodels.example.com/graph/integrations/neo4j/reaudit/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"projectName": null, "spaces": null}'

projectName: null means the most recently stored snapshot. The response is the audit-report shape above, with one extra metric (Snapshot stored, the timestamp the snapshot was persisted). If no snapshot exists — because nothing was imported, or because import hit the size cap — the call fails with a message that says exactly that and tells you to import first.

History — GET graph/integrations/neo4j/history/{projectId} (Viewer)

The rolling trail, newest run first, capped at the 50 most recent runs per estate:

{ "success": true, "vendor": "neo4j",
  "projects": [
    { "projectName": "neo4j",
      "runs": [
        { "at": "2026-08-04T05:00:11.9070000+00:00", "trigger": "scheduled",
          "errorCount": 0, "warningCount": 1, "infoCount": 1,
          "codes": { "label-no-unique-id": 1, "polymorphic-relationship": 1 },
          "fingerprint": "3f9a1c0d47b28e65" }
      ] }
  ] }

Each record is deliberately compact — counts, codes, fingerprint — because the trail answers "is this estate drifting over time?" while the live audit answers "what exactly is wrong now?". trigger identifies which loop recorded the run: audit (interactive with recordHistory), ci (the v1 surface), reaudit, or scheduled (the server-side heartbeat).

Badge — GET graph/integrations/neo4j/badge/{projectId} (Viewer)

Returns image/svg+xml: a self-contained shields-style badge built from the latest recorded run. Green (#4c1) when clean, yellow (#dfb317) for warnings only, red (#e05d44) with the error count, gray (#9f9f9f) when nothing has been recorded yet. Because it reads recorded history, the badge cannot show green out of optimism — it shows whatever your gates and heartbeats actually ran.

Generate — POST graph/integrations/neo4j/generate/{projectId} (Viewer)

Neo4j declares the Generate capability, and generation closes the loop the other way: governed meaning becomes vendor enforcement.

curl -sS -X POST \
  "https://coremodels.example.com/graph/integrations/neo4j/generate/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"typeNames": ["Person"], "targetVersion": null, "extra": {}, "spaces": null}'

typeNames restricts output to named governed types (empty covers everything eligible). targetVersion and extra exist on the shared request shape for vendors with dialect variants; the Neo4j generator needs neither.

{ "success": true,
  "artifacts": [
    { "name": "coremodels_constraints.cypher", "kind": "cypher",
      "content": "// Generated by CoreModels — governed graph schema constraints.\n// Meaning changes belong in CoreModels; regenerate this script rather than editing it.\n// NOT NULL (property existence) constraints require Neo4j Enterprise.\n\nCREATE CONSTRAINT person_email_unique IF NOT EXISTS FOR (n:Person) REQUIRE n.email IS UNIQUE;\nCREATE CONSTRAINT person_name_exists IF NOT EXISTS FOR (n:Person) REQUIRE n.name IS NOT NULL;\n" }
  ],
  "lossiness": [], "errors": [] }

One artifact, always: uniqueness and existence constraints from governed checks, plus indexes for taxonomy-constrained properties, every statement IF NOT EXISTS so the script is safe to run on every deploy. Reference Elements are excluded by design — in Neo4j a reference is a relationship, not a property, so a property constraint would enforce the wrong thing. If the governed model holds no uniqueness or existence facts at all, generation refuses rather than emitting an empty file that looks like enforcement: No governed uniqueness/existence facts found to emit as constraints.

Status — GET graph/integrations/neo4j/status/{projectId} (Viewer)

{ "success": true, "vendor": "neo4j", "imported": true,
  "state": {
    "vendor": "neo4j", "projectName": "neo4j",
    "importedAt": "2026-08-04T09:12:44.1180000+00:00",
    "sourceFingerprint": "3f9a1c0d47b28e65",
    "counts": "…", "facts": "…"
  },
  "governedDatasets": 3 }

imported: false with a null state means no import has run in this project yet — the cheapest pre-flight there is. governedDatasets counts imported Neo4j identities that currently resolve to governed Types, which makes it a useful counterweight to the audit's Datasets (estate) metric: if the estate reports fewer datasets than the project governs, a label disappeared.

The v1 machine surface

POST /v1/{projectId}/integrations/neo4j/audit
GET  /v1/{projectId}/integrations/neo4j/badge

The audit takes the identical request body, but the response is wrapped in the standard API envelope, so the report fields live under data.* — your gate reads data.errorCount. Runs recorded here carry trigger ci. Note what is deliberately not here: reaudit and history live on the interactive surface only, and import and generate are not exposed to API keys at all.

One project-level route

There is also POST graph/integrations/reconcile/{projectId} (Admin), which links datasets that two vendor estates govern as the same physical relation, using reciprocal sameAs assertions; the body names the two vendor keys (vendorA, vendorB). It matches on the normalized physical relation name; for Neo4j that name is the label itself, so pairs only form where another estate names a relation identically. It is idempotent — re-running refreshes the links rather than duplicating them.

Beyond the audit loop, a newer sync-plan surface exists on the interactive side — POST graph/integrations/neo4j/sync/propose/{projectId} (Viewer), plus project-level plan and ledger reads under graph/integrations/sync/… — which turns fresh artifacts into a reviewable change plan without touching governed meaning.

Failure modes

  • Unknown vendor keysuccess: false with every registered key listed back to you: Unknown vendor 'neo'. Registered: <comma-joined connector keys>.
  • Missing artifactsBody must include 'artifacts': { "<name>": "<content>" } (e.g. manifest for dbt).
  • Missing meta_schemaThe 'meta_schema' artifact (apoc.meta.schema() output) is required.
  • Malformed meta_schema → either Not valid JSON: … or, when it parses but is not APOC-shaped, Expected one JSON object keyed by label/relationship name. A payload with no node entries at all is rejected with No node labels found in the meta schema. rather than imported as an empty estate.
  • Malformed constraints → never fatal. The artifact is ignored and a lossiness record explains why; a corrective input is not allowed to break the primary one.

For the extraction recipe behind these payloads, see the Neo4j quickstart in the CoreModels integration docs.