08.07 Building a Script Against the API
End-to-end example scripts (bash and Python) showing how to authenticate, list risks, create a risk, update a risk, and handle errors. Use these as starting points for real integrations; productionize by adding secrets management, retries, logging, and tests.
Why this matters
Most "how do I integrate with SimpleRisk" conversations end up at the same place: a script that authenticates, hits a few endpoints, processes the responses. The Swagger UI tells you what's possible; this article shows you what the working code looks like for the most common patterns. Use the examples as starting points; productionize from there.
Prerequisites
Before running these examples:
- A SimpleRisk instance you can hit — preferably non-production for testing.
- An API Extra-active install with the
apimaster toggle on. See API Overview. - An API key for an integration user with appropriate permissions. See API Key Management.
- The base URL of your instance.
- For bash examples:
curlandjqinstalled. - For Python examples: Python 3.8+ and the
requestslibrary (pip install requests).
In all examples below, replace placeholders:
{your-simplerisk-instance}→ e.g.,simplerisk.example.com.{your-api-key}→ the 64-character API key you generated.
Example 1: List risks (bash)
The simplest integration: authenticate and read the risk list.
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="https://{your-simplerisk-instance}"
API_KEY="${SIMPLERISK_API_KEY:?Set SIMPLERISK_API_KEY environment variable}"
response=$(curl -sS \
-H "X-API-KEY: ${API_KEY}" \
-H "Accept: application/json" \
"${BASE_URL}/api/v2/risks")
# Check the response status code
status=$(echo "${response}" | jq -r '.status_code')
if [ "${status}" != "200" ]; then
echo "Error: status ${status}, message: $(echo "${response}" | jq -r '.status_message')" >&2
exit 1
fi
# Print risk count and a summary
count=$(echo "${response}" | jq '.data | length')
echo "Found ${count} risks"
echo "${response}" | jq -r '.data[] | "\(.id): \(.subject) (status: \(.status))"'
Save as list_risks.sh, make executable (chmod +x list_risks.sh), set SIMPLERISK_API_KEY in your environment, run.
The pattern: curl with the header, jq to parse JSON, error-check on status_code. Adapt to your needs.
Example 2: List risks (Python)
The same operation in Python:
#!/usr/bin/env python3
"""List risks from SimpleRisk."""
import os
import sys
import requests
BASE_URL = "https://{your-simplerisk-instance}"
API_KEY = os.environ.get("SIMPLERISK_API_KEY")
if not API_KEY:
print("Set SIMPLERISK_API_KEY environment variable", file=sys.stderr)
sys.exit(1)
response = requests.get(
f"{BASE_URL}/api/v2/risks",
headers={
"X-API-KEY": API_KEY,
"Accept": "application/json",
},
timeout=30,
)
if response.status_code != 200:
print(f"Error: status {response.status_code}, body: {response.text}", file=sys.stderr)
sys.exit(1)
data = response.json()
risks = data.get("data", [])
print(f"Found {len(risks)} risks")
for risk in risks:
print(f" {risk['id']}: {risk['subject']} (status: {risk.get('status')})")
Save as list_risks.py, set SIMPLERISK_API_KEY, run with python3 list_risks.py.
Example 3: Create a risk (Python)
A more involved example: submit a new risk via POST /api/v2/risks/submit.
#!/usr/bin/env python3
"""Submit a new risk to SimpleRisk."""
import os
import sys
import requests
BASE_URL = "https://{your-simplerisk-instance}"
API_KEY = os.environ.get("SIMPLERISK_API_KEY")
if not API_KEY:
print("Set SIMPLERISK_API_KEY environment variable", file=sys.stderr)
sys.exit(1)
# The new risk's data
new_risk = {
"subject": "Vendor X has not completed annual security questionnaire",
"reference_id": "VRM-2026-042",
"regulation": 1, # ID of the relevant regulation lookup
"control_number": "CC9.2", # the control reference
"location": [3], # location IDs (an array because location is multi-select)
"source": 2, # source ID
"category": 5, # category ID
"team": [4, 7], # team IDs the risk should be visible to
"technology": [1, 2], # technology IDs
"owner": 12, # owner user ID
"manager": 8, # manager user ID
"assessment": "The vendor's annual security questionnaire was due 2026-03-31 and has not been received. Without it we lack confirmation of their security posture.",
"notes": "Initial follow-up sent 2026-04-15; second follow-up 2026-04-30.",
# Scoring (using Classic methodology — adjust per your install)
"scoring_method": 1,
"CLASSIC_likelihood": 3,
"CLASSIC_impact": 4,
}
response = requests.post(
f"{BASE_URL}/api/v2/risks/submit",
headers={
"X-API-KEY": API_KEY,
"Accept": "application/json",
"Content-Type": "application/json",
},
json=new_risk,
timeout=30,
)
if response.status_code not in (200, 201):
print(f"Error: status {response.status_code}", file=sys.stderr)
print(f"Response: {response.text}", file=sys.stderr)
sys.exit(1)
result = response.json()
risk_id = result.get("data", {}).get("id")
print(f"Created risk {risk_id}")
The exact field set varies by your install's configuration (custom fields, scoring methodology, dropdown values). Use the Swagger UI's POST /risks/submit schema as the canonical reference for what fields are accepted.
Example 4: Update a risk (Python)
Modify an existing risk via PATCH /api/v2/risks/{id}:
#!/usr/bin/env python3
"""Update a risk's status in SimpleRisk."""
import os
import sys
import requests
BASE_URL = "https://{your-simplerisk-instance}"
API_KEY = os.environ.get("SIMPLERISK_API_KEY")
RISK_ID = sys.argv[1] if len(sys.argv) > 1 else None
if not API_KEY:
print("Set SIMPLERISK_API_KEY environment variable", file=sys.stderr)
sys.exit(1)
if not RISK_ID:
print("Usage: update_risk.py {risk_id}", file=sys.stderr)
sys.exit(1)
# Fields to update
update = {
"status": "Closed",
"close_reason": 1,
"notes": "Closed via integration; vendor questionnaire received 2026-05-12.",
}
response = requests.patch(
f"{BASE_URL}/api/v2/risks/{RISK_ID}",
headers={
"X-API-KEY": API_KEY,
"Accept": "application/json",
"Content-Type": "application/json",
},
json=update,
timeout=30,
)
if response.status_code != 200:
print(f"Error: status {response.status_code}", file=sys.stderr)
print(f"Response: {response.text}", file=sys.stderr)
sys.exit(1)
print(f"Updated risk {RISK_ID}")
The PATCH endpoint accepts only the fields you want to change; omitted fields are unchanged.
Example 5: Polling for changes
For "near-real-time" integration without webhooks: poll the risk list periodically, dispatch on changes.
#!/usr/bin/env python3
"""Poll SimpleRisk for risk changes and dispatch to a downstream system."""
import json
import os
import sys
import time
from pathlib import Path
import requests
BASE_URL = "https://{your-simplerisk-instance}"
API_KEY = os.environ.get("SIMPLERISK_API_KEY")
STATE_FILE = Path("./risk_state.json")
POLL_INTERVAL = 300 # 5 minutes
def fetch_risks():
response = requests.get(
f"{BASE_URL}/api/v2/risks",
headers={"X-API-KEY": API_KEY, "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
return {r["id"]: r for r in response.json().get("data", [])}
def load_state():
if STATE_FILE.exists():
return json.loads(STATE_FILE.read_text())
return {}
def save_state(state):
STATE_FILE.write_text(json.dumps(state))
def dispatch_change(event_type, risk):
"""Replace this with your downstream integration (Slack post, etc.)"""
print(f"[{event_type}] {risk['id']}: {risk['subject']}")
def main():
while True:
try:
current = fetch_risks()
previous = load_state()
# New risks
for risk_id, risk in current.items():
if str(risk_id) not in previous:
dispatch_change("NEW", risk)
# Removed risks
for risk_id in previous:
if risk_id not in {str(k) for k in current}:
dispatch_change("REMOVED", {"id": risk_id, "subject": "(removed)"})
# Update state
save_state({str(k): v for k, v in current.items()})
except requests.RequestException as e:
print(f"Polling failed: {e}", file=sys.stderr)
# Don't update state on failure; retry next cycle
time.sleep(POLL_INTERVAL)
if __name__ == "__main__":
main()
This is a starting-point pattern. Productionize:
- Detect changes (not just new/removed) by comparing fields.
- Use exponential backoff on persistent failures.
- Log to a structured logger, not stdout.
- Run as a long-running service (systemd, Docker, k8s) rather than a script.
- Add metrics (polls per minute, dispatch count, error rate).
Productionizing scripts
Scripts that work in dev need work to be production-ready:
Secrets management
Don't export SIMPLERISK_API_KEY=... in a developer's shell history. Use:
- A secrets manager (Vault, AWS Secrets Manager, Azure Key Vault) — fetch the key at runtime.
- Encrypted environment files (sops, encrypted .env) loaded by the deployment environment.
- Per-developer keys with short rotation cycles for development; per-environment keys for staging/production.
Error handling and retries
Real integrations encounter:
- Transient network errors (retry with backoff).
- 5xx errors from SimpleRisk during deploys (retry with backoff).
- 401/403 errors (don't retry; alert).
- 429 errors if a reverse proxy enforces rate limits (retry with
Retry-After). - Malformed responses (don't crash; log and skip).
Use a sturdy HTTP client library (Python's requests with urllib3.util.retry.Retry, or higher-level libs like tenacity) that handles retry/backoff without you reinventing the wheel.
Logging
Log structured events: timestamp, request URL, response status, response time, integration-specific context. Avoid logging full response bodies (risk descriptions can contain sensitive content). Send logs to a centralized aggregator (ELK, Splunk, Datadog) for searchability and alerting.
Testing
Write tests:
- Unit tests for the parsing and processing logic (mock the API responses).
- Integration tests against a non-production SimpleRisk instance (use a dedicated test user / API key).
- Smoke tests that run after deployment to confirm the integration is working end-to-end.
Monitoring and alerting
Production integrations should emit metrics: requests per minute, error rate, dispatch latency. Alert on:
- Error rate above a threshold (the integration is failing).
- Zero requests for an extended period (the integration is hung).
- Authentication failures (the key was rotated or revoked).
Deployment
Run the integration as a long-running service:
- Cron for periodic scripts.
- systemd services for long-running daemons.
- Docker / Kubernetes for containerized deployments.
- AWS Lambda / Cloud Functions for event-driven serverless patterns.
The choice depends on the integration's lifecycle and your operational stack.
Common pitfalls
A handful of patterns recur with API scripts.
-
Hardcoding the API key in the script. Use environment variables or a secrets manager.
-
Hardcoding the base URL. Same — use environment variables. Same script should work against dev, staging, and production with only configuration changes.
-
No timeout on requests. A hung SimpleRisk causes the script to hang indefinitely. Always set a timeout.
-
No error handling. Production scripts encounter errors; handle them gracefully.
-
Polling at high frequency. 1-second polling is rarely necessary; 5-minute polling is plenty for most use cases. Be a good citizen.
-
Treating the script as the integration. A bash script in a developer's home directory isn't an integration. Move it to a deployment surface (cron, systemd, Docker) with monitoring and operational ownership.
-
Not version-pinning dependencies. A Python script that uses
requests==2.30.0in dev but the latest in production might behave differently. -
Not documenting the script. Future-you (or your colleagues) will need to know what it does, why, and how to operate it.
-
Blocking the main thread on long operations. A script that does a long-running operation per risk in a loop can take hours; consider concurrency (with appropriate rate limiting) or async patterns.
-
Logging API keys, full responses with sensitive content, or other secrets. Restrain what you log.