Developers
ROADMAP-V71 B5. One page that answers "can I integrate this?" in a minute — the front door for the 8 files this directory already had no index for. Every claim below is sourced from the files it links to; none of it is restated prose that could drift from them.
Public API v2 quickstart
Base URL: https://app.cmdash.net/api (self-hosted: your own origin).
Auth: Authorization: Bearer cmdash_ek_... — mint a key at Settings →
API Keys (Enterprise plan, Owner or Admin role). Full auth, scope and
rate-limit detail: public-api-versioning.md.
Read (GET /v2/posture/latest, scope posture:read):
curl https://app.cmdash.net/api/v2/posture/latest \
-H "Authorization: Bearer cmdash_ek_..."
{
"success": true,
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-03-31T12:00:00.000Z",
"data": {
"score": 74,
"grade": "C",
"delta": 3,
"computedAt": "2026-03-31T12:00:00.000Z",
"signals": []
}
}
Write (POST /v2/risks, scope risks:write),
with an idempotency key so a retried request never double-creates:
curl -X POST https://app.cmdash.net/api/v2/risks \
-H "Authorization: Bearer cmdash_ek_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: r9f7d2c4-e1a8-44b3-9c7e-3f8b1e9d0a2c" \
-d '{"title":"Unpatched critical vulnerabilities in production","likelihood":4,"impact":5,"category":"Vulnerability Management"}'
{
"success": true,
"requestId": "9b1f0e2a-6c4d-4e3a-8f2b-1d7c5a9e0f3b",
"timestamp": "2026-03-31T12:00:01.000Z",
"data": {
"id": "risk-uuid-001",
"riskId": "RSK-001",
"title": "Unpatched critical vulnerabilities in production",
"likelihood": 4,
"impact": 5,
"riskScore": 20,
"status": "OPEN"
}
}
Every route, scope and rate limit, generated from the controllers:
public-api-reference.md. What v1 promises and
why v2 exists: public-api-versioning.md. A
vCISO or MSP practice reading the client workspaces it owns uses a PARTNER key:
partner-api.md.
What is capped, how a limit answers, and what a webhook does and does not guarantee:
limits.md.
Try it against the sandbox
ROADMAP-V80 H7. You do not need an account to make the first call.
https://demo.cmdash.net is a public, read-only workspace full of sample
data, rebuilt from the seed every night. Mint yourself a key, then use it:
CMDASH_SANDBOX_KEY=$(curl -s -X POST https://demo.cmdash.net/api/sandbox/key | jq -r .data.key)
curl https://demo.cmdash.net/api/v2/posture/latest \
-H "Authorization: Bearer $CMDASH_SANDBOX_KEY"
(.data.key, not .key — like every other CMDash API response, this one is
wrapped in the standard { success, requestId, timestamp, data } envelope.)
ROADMAP-V100 I5. POST /sandbox/key is anonymous, rate-limited (the same
per-IP/hour ceiling as POST /auth/sandbox-session), and mints a fresh key
scoped to the sandbox tenant only, carrying every :read scope the Public
API defines. It expires in 24 hours (or at the next nightly sandbox reset,
whichever comes first) — mint another one when it does, the same way. Like
any CMDash API key, it is shown once, in the response above; there is no way
to look its raw value up again afterward, self-serve-minted or not.
If you would rather not mint your own — a fixed value to paste into a CI
example, say — a second, static, never-expiring sandbox key is published the
same way it always has been: minted by docs/runbooks/sandbox-first-run.md
Step 2 and kept in the ops secrets vault, ask whoever provisioned the sandbox
for its value. Both keys work identically against the sandbox tenant; only
POST /sandbox/key is self-serve. Swap demo.cmdash.net for
app.cmdash.net and your own tenant's key (Settings → API Keys) when you
move to real data — nothing else in the request changes.
What the sandbox key can and cannot do, and why:
| Reads | Every GET the Public API defines, on the sandbox tenant only. It carries every :read scope. |
| Writes | Refused, always — 403 SANDBOX_READ_ONLY, on every POST/PUT/PATCH/DELETE. Not "it has no write scope": the refusal is a global guard that runs before any route, so it does not depend on which scopes the key happens to hold. |
| Exports and downloads | Refused, including GET ones (/v2/audit-logs/export, /data-export/*, …/download). A public unauthenticated bulk-export endpoint is a free data-egress and PDF-rendering surface even when the data behind it is fake. |
| Other tenants | Invisible. sandbox:read is a restriction on top of tenant isolation, never a replacement for it — the sandbox key resolves to the sandbox tenant and RLS does the rest. |
| Lifetime | It does not expire, and it survives the nightly reset. The workspace behind it does not: everything you can see there is regenerated from the seed each night. |
The rule is a single check applied identically to browser sandbox sessions and to this key, and it is tested route by route against the whole API, not a sample.
API documentation formats:
- Download OpenAPI (JSON) — Import into any OpenAPI-compatible tool.
Covers both v1 and v2 in one document, tagged
v1 <resource>/v2 <resource>(v1 operations are also marked"deprecated": true) — the same v2 routes shown in the quickstart above are in here. - Browse the API reference — Every v1 and v2 operation, rendered in-site from the same document above (ROADMAP-V90 A6 — no more sending readers to petstore.swagger.io).
- Run in Postman — Download and import into Postman, or use Postman's import feature with the JSON file. Same v1+v2 coverage, one folder per resource — v2 folders first, the deprecated v1 folders after them (ROADMAP-V100 I3).
SDK
@cmdash/api-sdk — a hand-written, zero-dependency TypeScript client.
client.<namespace> is v2, the current version (ROADMAP-V100 I4);
client.v2.<namespace> is the same object spelled out, and the deprecated
v1 surface is at client.v1.<namespace>. Kept honest by a test that fails
CI if a route and an SDK method ever disagree, for either version. Install and first call, copied
verbatim from the package's own README:
import { CmdashClient } from '@cmdash/api-sdk';
const client = new CmdashClient({
apiKey: process.env.CMDASH_API_KEY!,
// baseUrl: 'https://app.cmdash.net/api', // default; override for on-prem
});
const posture = await client.posture.getLatest();
console.log(posture?.score, posture?.grade);
The full README, including error types and CI-pipeline guidance, is published with the package on npm.
Python: cmdash-api-sdk — generated from this same served spec
(/docs/openapi.json), zero dependencies, fully type-hinted. Install,
auth, three examples and how it's regenerated:
sdk-python.md.
Webhooks
CMDash pushes events to you: register a URL and events at
POST /v2/webhooks, verify each delivery's X-CMDash-Signature
(HMAC-SHA256, 5-minute replay window), and deduplicate on eventId — nine
event types, sequencing/gap-detection guarantees, retry/circuit-breaker and
rotation semantics: webhook-events.md.
Building the receiving end for a vendor pushing events into CMDash
(GitHub, Okta, Jira, ServiceNow, CrowdStrike) is a different guide, for a
different direction of traffic: webhook-receivers.md.
Connector development + conformance kit
A connector you contribute is a declarative JSON definition (an HTTP
request template + JSONPath field mappings) — never compiled code running
inside CMDash's process. Author it as your own custom connector, validate
the shape offline, then submit it for review. Full walkthrough, the
definition schema, and the security boundary this design enforces:
connector-development.md.
Status: usable today, no network required. The kit is a zero-dependency script — verified in this checkout:
node docs/developers/connector-conformance-kit/validate.mjs \
docs/developers/connector-conformance-kit/examples/valid-example.json
reports pass/fail plus the exact reason for each failure (a credential-shaped
header, an unsupported JSONPath filter, a non-frozen observation type, a
duplicate target field on the invalid fixture). Details:
connector-conformance-kit/README.md.
Terraform provider
terraform-provider-cmdash manages what the public API v2 can actually
write today — full CRUD is only real for cmdash_webhook_registration
(GET/POST/PUT/DELETE /v2/webhooks); frameworks and integrations are
read-only data sources; policy, custom roles and alert rules have no
public API route at all yet, so those three resources are not implemented
rather than faked.
Status: builds clean from this checkout (cd terraform-provider-cmdash && go build . — verified). Unit tests need no network; acceptance tests run
against an in-process HTTP fake, gated by TF_ACC=1. The full resource
matrix, gap list, local dev_overrides setup and testing instructions:
terraform-provider.md.
Idempotency
Every POST/PUT/PATCH/DELETE on the public API accepts an
Idempotency-Key header (Stripe's model): same key + same body within 24h
returns the exact cached response instead of re-running the side effect;
same key with a different body is a 422; a key already in flight is a
409. Format, server-side state machine, and the client retry pattern:
idempotency.md. Uploads are covered too: the
fingerprint includes the file's bytes, so a retry that keeps the form and
swaps the file is a 422, not the first file's cached response.
List conventions
Every internal list endpoint takes ?limit&offset&sort&dir&q and answers
with { items, totalCount, hasMore, limit, offset }. Six competing
conventions were measured before this; the older spellings (page,
pageSize, search, sortBy, sortDir) are still accepted, resolved
onto the canonical ones, so no saved URL breaks. The public API (/v1,
/v2) is exempt by design — it is versioned, SDK-consumed and under an
active Sunset contract: list-conventions.md.
Optimistic concurrency
The internal API's four long-form editors (risk, policy metadata, report
template, custom maturity model) return a strong ETag on their
single-entity read and honour If-Match on the save: a stale write is
refused with 412 Precondition Failed (details.code
PRECONDITION_FAILED) and nothing is written, while a request that sends no
If-Match is unaffected. Which routes, how the token is derived, why the
check is atomic rather than advisory, and the client retry pattern:
optimistic-concurrency.md.
Data export format
POST /data-export (OWNER only) produces a full-fidelity JSON archive of
every tenant-scoped model, generated from the live Prisma schema so a new
model is included by default rather than by an edit someone has to
remember. Paired with a deletion certificate on offboarding that states
exactly what was executed and what it does not claim:
data-export-format.md.
Versioning
/v1/* is deprecated (not removed) as of 2026-08-24, Sunset 2027-02-24;
/v2/* is the same auth, rate limits and envelope with two 200 { error }
endpoints fixed to real 404s and six previously-unenforced scopes wired
up. What "breaking" means here, the full deprecation-header contract, and
the SDK's v1 targeting: public-api-versioning.md.
Self-hosting
CMDash runs on your own hardware or in your own AWS account, on the same code the hosted service runs.
- Your own Docker host:
self-host.mdcovers the install, preflight and first-login path. The install is licensed by a signedlicense.json(a 30-day self-host trial is one per account and watermarks the install as an evaluation). - Your own AWS account:
self-host-aws.mdcovers three CloudFormation templates indeploy/aws/(evaluation, production with a load balancer and RDS, and GovCloud) that launch the CMDash AMI. TheLicensingparameter chooses the entitlement source once, at launch:license-file(a signed CMDash licence file you upload in the app) ormarketplace(the plan comes from your AWS Marketplace subscription, read through AWS License Manager). - Licence verification is entirely offline for a licence file, so a self-hosted deployment never has to contact CMDash to check one.
- Air-gapped installation: the distributable bundle ships 3 of the 9 images a full stack needs, and an unsigned bundle is rejected by default, so use the connected install paths above for a fresh host.
Community
ROADMAP-V80 J4. Questions about the SDK or the Terraform provider, real-time chat, and office hours: cmdash.com/community.