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:
- The key always starts with
sk-. - There is no
X-API-Keyheader — the key rides onAuthorization: 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_keyis 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
EvidenceLineagerow 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 withcreated: 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_inseconds); upload before it expires. Files larger thanmax_sizeare rejected by storage.link-to-control→{ lineage_id, evidence_id, control_id, framework_id, assessment_id, created }.created: falsemeans the link already existed (safe to retry).503fromupload-url/verify→ object storage is not configured (see the prerequisite note).403 missing required scope→ the API key lackswrite: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 thatorg_id; this is the RBAC layer, not a client bug.