Seven Verbs and Two Surfaces: The Complete Salesforce Integration API
Before writing a single call, it pays to see the whole map. The Salesforce integration in CoreModels (vendor key `salesforce`) is small enough to hold in your head: the seven core per-vendor verbs on the interactive HTTP surface, two on the machine-to-machine surface, one artifact, and two roles. The connector declares all three capabilities — **Import, Audit, Generate** — so every verb below is live for Salesforce; nothing in this reference is aspirational.
Seven Verbs and Two Surfaces: The Complete Salesforce Integration API
Before writing a single call, it pays to see the whole map. The Salesforce integration in CoreModels (vendor key salesforce) is small enough to hold in your head: the seven core per-vendor verbs on the interactive HTTP surface, two on the machine-to-machine surface, one artifact, and two roles. The connector declares all three capabilities — Import, Audit, Generate — so every verb below is live for Salesforce; nothing in this reference is aspirational.
The two surfaces differ by who is calling:
- Interactive —
graph/integrations/..., authenticated with your normal CoreModels login token. The full verb set lives here. - Machine-to-machine —
v1/..., which accepts user API keys. This is what CI and unattended callers use. It carriesauditandbadge;reauditandhistoryare interactive-only.
Every route is authenticated, and each call is additionally checked against your role in the target project: Admin for the one verb that writes governed content, Viewer for everything else. {PROJECT_ID} is the project's 32-character hex id; the base URL is written as https://coremodels.example.com.
Discovery — GET graph/integrations/vendors
Any authenticated user can ask the API what it supports:
GET https://coremodels.example.com/graph/integrations/vendors
Authorization: Bearer $TOKEN
The response lists every registered connector with its key, display name, capability flags, and artifact contract. The Salesforce entry looks like this:
{ "key": "salesforce", "displayName": "Salesforce",
"capabilities": "Import, Audit, Generate",
"artifacts": {
"describe": "required — JSON array of sObject describe results (GET /services/data/vXX.0/sobjects/{name}/describe, aggregated)"
} }
Calling any route with a key the registry doesn't know returns success: false with a message naming every registered key — the API tells you what it knows rather than guessing.
Import — POST graph/integrations/salesforce/import/{PROJECT_ID} (Admin)
The one writing verb, and it writes additively: existing governed nodes are never mutated or deleted on re-import; only new estate facts are added, and vendor bookkeeping metadata is refreshed.
POST https://coremodels.example.com/graph/integrations/salesforce/import/{PROJECT_ID}
Authorization: Bearer $TOKEN
Content-Type: application/json
{ "artifacts": { "describe": "<contents of describe.json>" } }
The body contract is shared with audit: artifacts maps artifact name to raw content as a string, and an optional spaces array of space ids targets part of the project (empty means the main space). Omit artifacts and you get a success: false error whose message spells out the required shape.
The response reports datasetsAdded, datasetsSkippedExisting, fieldsAdded, lineageEdgesAdded, lineageEdgesSkipped, nodesEnriched, a snapshotStored flag, and the lossiness and errors channels. Two Salesforce-specific readings: lineageEdgesAdded is 0 today — child relationships are not yet mapped to lineage, and we would rather report an honest zero than invent a DAG — and snapshotStored: false (with a lossiness record) means the org was too large for the snapshot cap of roughly 1.5 MB encoded, in which case fresh-artifact audits still work but re-audit has nothing stored to run against.
Audit — POST graph/integrations/salesforce/audit/{PROJECT_ID} (Viewer)
Read-only, always. The optional recordHistory flag (default false) is the single opt-in side effect: it appends the run to the project's audit history.
POST https://coremodels.example.com/graph/integrations/salesforce/audit/{PROJECT_ID}
Authorization: Bearer $TOKEN
Content-Type: application/json
{ "artifacts": { "describe": "<contents of describe.json>" },
"spaces": [],
"recordHistory": true }
The response is the richest payload on the surface:
{
"success": true,
"vendor": "salesforce",
"projectName": "salesforce-org",
"errorCount": 0, // the CI-gate number: > 0 means governed meaning is violated
"warningCount": 2,
"infoCount": 5,
"codes": { "picklist-unrestricted": 2, "field-no-help": 5 },
"driftedObjects": [], // vendor identities of anything that drifted
"fingerprint": "b1946ac92492d234",
"metrics": { },
"findings": [ /* { section, severity, code, subject, message, detail } */ ],
"markdown": "...", // the complete human-readable report
"historyRecorded": true,
"lossiness": []
}
Findings are grouped into Coverage (dataset-unmapped, field-unmapped), Drift (dataset-removed, field-removed, field-type-drift, enum-constraint-removed, enum-narrowed, enum-widened, contract-drift), and Conformance, where the Salesforce connector contributes three rules of its own: field-no-help (Info — a custom field without inline help text), picklist-unrestricted (Warning — the platform does not enforce the value set), and polymorphic-reference (Info — only the first lookup target is governed as a reference).
Re-audit — POST graph/integrations/salesforce/reaudit/{PROJECT_ID} (Viewer)
Audit answers "do these fresh artifacts still conform to the governed model?" Re-audit asks the mirror question: "after the governed model changed, does the last-known org still conform?" It runs the same audit engine over the snapshot stored at import time against the current governed model — no artifacts, no Salesforce connection:
POST https://coremodels.example.com/graph/integrations/salesforce/reaudit/{PROJECT_ID}
Authorization: Bearer $TOKEN
Content-Type: application/json
{}
An optional "projectName" selects which stored snapshot to re-audit (null means the latest). The response has the same shape as audit, with one difference in posture: a re-audit run is always recorded in the history — that is the verb's purpose.
History — GET graph/integrations/salesforce/history/{PROJECT_ID} (Viewer)
The rolling audit trail, newest first and capped:
{ "success": true, "vendor": "salesforce",
"projects": [
{ "projectName": "salesforce-org",
"runs": [
{ "at": "2026-08-03T09:12:44Z", "trigger": "ci",
"errorCount": 0, "warningCount": 2, "infoCount": 5,
"codes": { "picklist-unrestricted": 2, "field-no-help": 5 },
"fingerprint": "b1946ac92492d234" } ] } ] }
The trigger tells you how each run got there: an explicit audit with recordHistory, a re-audit, a CI call, or the scheduled heartbeat.
Badge — GET graph/integrations/salesforce/badge/{PROJECT_ID} (Viewer)
Returns image/svg+xml rendered from the latest recorded run: green for clean, yellow for warnings only, red for errors, gray for no recorded runs. The label reads salesforce audit. Runs only enter the history when a caller opts in (or re-audits), so the badge reflects deliberate checkpoints, not incidental traffic.
Generate — POST graph/integrations/salesforce/generate/{PROJECT_ID} (Viewer)
Generation closes the loop in the other direction: governed model → Metadata-API CustomObject XML scaffolds, one objects/{ApiName}.object artifact per governed Type.
POST https://coremodels.example.com/graph/integrations/salesforce/generate/{PROJECT_ID}
Authorization: Bearer $TOKEN
Content-Type: application/json
{ "typeNames": ["Invoice__c"] }
typeNames restricts generation (empty means everything eligible). The request shape also accepts targetVersion and extra for connectors that need dialect switches; the Salesforce generator keys off typeNames. The response carries the artifacts inline:
{ "success": true,
"artifacts": [
{ "name": "objects/Invoice__c.object", "kind": "xml", "content": "<?xml version=\"1.0\" ..." } ],
"lossiness": [
{ "kind": "ConstraintRelaxation", "path": "Invoice__c.Is Paid",
"explanation": "Salesforce checkboxes cannot be required; the constraint was dropped." } ],
"errors": [] }
Inside the XML: governed taxonomies become restricted picklist value sets, governed references become Lookup fields with minted relationship names, NotNull checks become required (checkboxes excepted — hence the lossiness record above), and Unique checks become unique. Standard fields (Id, Name, the audit fields, OwnerId) are never scaffolded, and governed-first fields that never existed in Salesforce are minted with the __c suffix. These are review-and-deploy scaffolds for your own change process — CoreModels never deploys to your org, and the generated file says so in a comment at the top.
Status — GET graph/integrations/salesforce/status/{PROJECT_ID} (Viewer)
The last-import state at a glance:
{ "success": true, "vendor": "salesforce", "imported": true,
"state": { /* versions, timestamps, fingerprint, counts */ },
"governedDatasets": 4 }
imported: false simply means no Salesforce import has ever run in this project.
The machine-to-machine surface
Two routes accept user API keys, both Viewer-role and both read-only:
POST https://coremodels.example.com/v1/{PROJECT_ID}/integrations/salesforce/audit
GET https://coremodels.example.com/v1/{PROJECT_ID}/integrations/salesforce/badge
The v1 audit takes the same body as the interactive one but wraps its response in an envelope: success at the top level, everything else under data — so a CI script tests data.errorCount. When recordHistory is true here, the run is recorded with the CI trigger. The badge is byte-identical to the interactive one, embeddable wherever you can attach the key.
Roles at a glance
| Verb | Route | Role |
|---|---|---|
| Discovery | GET graph/integrations/vendors | any authenticated |
| Import | POST .../salesforce/import/{PROJECT_ID} | Admin |
| Audit | POST .../salesforce/audit/{PROJECT_ID} | Viewer |
| Re-audit | POST .../salesforce/reaudit/{PROJECT_ID} | Viewer |
| History | GET .../salesforce/history/{PROJECT_ID} | Viewer |
| Badge | GET .../salesforce/badge/{PROJECT_ID} | Viewer |
| Generate | POST .../salesforce/generate/{PROJECT_ID} | Viewer |
| Status | GET .../salesforce/status/{PROJECT_ID} | Viewer |
| CI audit / badge | POST / GET v1/{PROJECT_ID}/integrations/salesforce/... | Viewer (API keys) |
That Viewer-heavy column is deliberate: everything except import is read-only, so the credentials you spread around — CI keys, agent tokens — are the ones that cannot change governed meaning.
What is deliberately not here
There is no live-connection route. LiveSync is a declared-but-deferred capability: CoreModels never holds Salesforce credentials, and every artifact is something you exported yourself. Describe carries inline help text but not Metadata-API long descriptions, so those are not extracted; child relationships are not yet lineage. Each of those limits is stated rather than papered over — the same honesty the lossiness channel applies per call.
One newer surface is out of scope here rather than missing: the sync-plan routes (POST graph/integrations/salesforce/sync/propose/{PROJECT_ID}, plus plan fetch and a sync ledger under graph/integrations/sync/...), which classify fresh artifacts into a reviewable, replayable change plan instead of a report. Audit and re-audit remain the drift verbs this reference covers.
The full worked example of every call above, with the extraction recipe included, is the Salesforce quickstart in the CoreModels docs (quickstarts/salesforce).