Skip to content
English
  • There are no suggestions because the search field is empty.

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 api master 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: curl and jq installed.
  • For Python examples: Python 3.8+ and the requests library (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.0 in 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.

Related