Skip to content

Integrations

This page is a quickstart for the evidence-collection middleware flow: programmatically pushing a compliance artifact into Sovereign GRC and linking it to a control, using an API key. It is the path you wire into CI/CD jobs, cloud-config exporters, or any automated evidence collector.

If you only need read access (dashboards, posture sync, an LLM agent), see MCP Integration, which wraps the same API.

Authentication

All programmatic calls use a customer-managed API key on the standard Authorization header:

Authorization: Bearer sk-xxxxxxxxxxxxxxxx
  • The key always starts with sk-.
  • There is no X-API-Key header — the key rides on Authorization: Bearer.
  • API-key requests are exempt from CSRF (CSRF only applies to cookie/session calls).

Create and scope keys from the API Keys page (a key is shown once — copy it immediately).

Evidence scopes

API keys are scoped. The evidence flow needs:

Scope Grants
write:evidence Request upload URLs and link evidence to controls.
read:evidence List evidence, read metadata, request download URLs.
read:* / write:* Wildcards that cover all read / all write scopes respectively.

A key with write:evidence (or write:*) can run the entire upload → link flow below. If a call returns 403 missing required scope, widen the key's scopes on the API Keys page.

The evidence flow

Pushing an artifact is a four-step handshake. The platform never receives your file through its own API — it hands you a presigned URL and you PUT the bytes straight to object storage, then tell the platform where it landed.

1. POST .../evidence/upload-url      → { upload_url, object_key, expires_in, max_size }
2. PUT  <upload_url>  (raw bytes)     → 200/204 from object storage
3. POST .../evidence/verify          → confirm integrity (SHA-256)        [optional]
4. POST .../evidence/{id}/link-to-control → associate the object with a control
  • object_key is the handle for the stored object; carry it through every later step.
  • Step 3 (verify) is the confirmation step: it recomputes the stored object's SHA-256 and checks it against the hash you computed locally, so you know the upload was not truncated or altered. It is optional but recommended for an audit-grade chain of custody.
  • Step 4 writes an EvidenceLineage row tying the object to a control (and optionally a framework/assessment). It is idempotent — re-linking the same object to the same control returns the existing association with created: false.

Object storage prerequisite

The upload/download URLs point at the configured S3-compatible store (Cloudflare R2 in production, MinIO for self-hosted). If storage is not configured, upload-url returns 503. See the Trust Center object-storage note for the required R2_* / CF_ACCOUNT_ID environment variables.

Endpoints

Method Path Purpose
POST /api/v1/orgs/{org_id}/evidence/upload-url Mint a presigned PUT URL + object_key.
PUT {upload_url} Upload raw file bytes to object storage.
POST /api/v1/orgs/{org_id}/evidence/verify Verify integrity (object_key, expected_hash).
POST /api/v1/orgs/{org_id}/evidence/{evidence_id}/link-to-control Link the object to a control.

The {evidence_id} in step 4 is the evidence's content hash / object id; pass the object_key in the body for traceability.

Samples

The samples below upload access_logs_2024.csv and link it to control CC6.1. Set these first:

export GRC_BASE_URL="https://demo.defendflow.xyz"
export GRC_ORG_ID="00000000-0000-0000-0000-000000000001"
export GRC_API_KEY="sk-...your-write-evidence-key..."
BASE="$GRC_BASE_URL/api/v1/orgs/$GRC_ORG_ID/evidence"
AUTH="Authorization: Bearer $GRC_API_KEY"
FILE="access_logs_2024.csv"
HASH=$(sha256sum "$FILE" | cut -d' ' -f1)

# 1) Request a presigned upload URL
RESP=$(curl -s -X POST "$BASE/upload-url" -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d "{\"filename\":\"$FILE\",\"content_type\":\"text/csv\",\"control_id\":\"CC6.1\"}")
UPLOAD_URL=$(echo "$RESP" | jq -r .upload_url)
OBJECT_KEY=$(echo "$RESP" | jq -r .object_key)

# 2) PUT the file bytes directly to object storage
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: text/csv" --data-binary "@$FILE"

# 3) (optional) confirm integrity
curl -s -X POST "$BASE/verify?object_key=$OBJECT_KEY&expected_hash=$HASH" -H "$AUTH"

# 4) Link the object to the control (idempotent)
curl -s -X POST "$BASE/$HASH/link-to-control" -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d "{\"control_id\":\"CC6.1\",\"framework_id\":\"SOC2\",\"object_key\":\"$OBJECT_KEY\"}"
import hashlib, os, requests

BASE = f"{os.environ['GRC_BASE_URL']}/api/v1/orgs/{os.environ['GRC_ORG_ID']}/evidence"
HEADERS = {"Authorization": f"Bearer {os.environ['GRC_API_KEY']}"}
PATH, CONTROL, FRAMEWORK = "access_logs_2024.csv", "CC6.1", "SOC2"

data = open(PATH, "rb").read()
file_hash = hashlib.sha256(data).hexdigest()

# 1) presigned upload URL
r = requests.post(f"{BASE}/upload-url", headers=HEADERS, json={
    "filename": PATH, "content_type": "text/csv", "control_id": CONTROL,
})
r.raise_for_status()
up = r.json()

# 2) PUT bytes to object storage (no auth header — the URL is presigned)
requests.put(up["upload_url"], data=data,
             headers={"Content-Type": "text/csv"}).raise_for_status()

# 3) (optional) verify integrity
requests.post(f"{BASE}/verify", headers=HEADERS,
              params={"object_key": up["object_key"], "expected_hash": file_hash})

# 4) link to control (idempotent)
r = requests.post(f"{BASE}/{file_hash}/link-to-control", headers=HEADERS, json={
    "control_id": CONTROL, "framework_id": FRAMEWORK, "object_key": up["object_key"],
})
print(r.json())   # -> { "lineage_id": "...", "created": true|false, ... }
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";

const BASE = `${process.env.GRC_BASE_URL}/api/v1/orgs/${process.env.GRC_ORG_ID}/evidence`;
const headers = { Authorization: `Bearer ${process.env.GRC_API_KEY}` };
const PATH = "access_logs_2024.csv", CONTROL = "CC6.1", FRAMEWORK = "SOC2";

const bytes = await readFile(PATH);
const fileHash = createHash("sha256").update(bytes).digest("hex");

// 1) presigned upload URL
const up = await fetch(`${BASE}/upload-url`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({ filename: PATH, content_type: "text/csv", control_id: CONTROL }),
}).then((r) => r.json());

// 2) PUT bytes straight to object storage (presigned, no auth header)
await fetch(up.upload_url, {
  method: "PUT",
  headers: { "Content-Type": "text/csv" },
  body: bytes,
});

// 3) (optional) verify integrity
await fetch(`${BASE}/verify?object_key=${encodeURIComponent(up.object_key)}` +
            `&expected_hash=${fileHash}`, { method: "POST", headers });

// 4) link to control (idempotent)
const link = await fetch(`${BASE}/${fileHash}/link-to-control`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({ control_id: CONTROL, framework_id: FRAMEWORK, object_key: up.object_key }),
}).then((r) => r.json());
console.log(link); // { lineage_id, created, ... }

Responses & errors

  • upload-url{ upload_url, object_key, expires_in, max_size }. The URL is short-lived (expires_in seconds); upload before it expires. Files larger than max_size are rejected by storage.
  • link-to-control{ lineage_id, evidence_id, control_id, framework_id, assessment_id, created }. created: false means the link already existed (safe to retry).
  • 503 from upload-url / verify → object storage is not configured (see the prerequisite note).
  • 403 missing required scope → the API key lacks write:evidence. Re-scope it on the API Keys page.
  • 401 / 403 not a member of this organization → the key resolves to a user without org membership for that org_id; this is the RBAC layer, not a client bug.