Python SDK
The official Python SDK is a small wrapper around the Vulnotes REST API. It handles authentication, pagination, file uploads, exports, retries, and API errors while keeping responses close to the JSON returned by the API.
The SDK requires Python 3.9 or later.
Installation
pip install vulnotesAuthentication
Create a key under Administration > Settings > API Keys. Only grant the permissions your script needs. See API Keys for the available scopes and how access is restricted.
Pass the instance URL and key when creating the client:
from vulnotes import VulnotesClient
client = VulnotesClient(
"https://acme.vulnotes.app",
api_key="vuln_sk_...",
)You can also keep the credentials out of your source code:
export VULNOTES_URL="https://acme.vulnotes.app"
export VULNOTES_API_KEY="vuln_sk_..."from vulnotes import VulnotesClient
client = VulnotesClient()The client adds /api to the instance URL when needed. Use the base URL of your Vulnotes instance, not the URL of a specific API endpoint.
Basic usage
SDK methods return regular Python dictionaries and lists. This makes it easy to use the result directly or pass it to an existing script.
from vulnotes import VulnotesClient
with VulnotesClient() as client:
report = client.reports.create(
"External pentest Q3",
language="EN",
)
client.findings.add(report["_id"], {
"title": "SQL injection in /login",
"severity": "Critical",
"cvss": {
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
},
"data": {
"EN": {
"title": "SQL injection in /login",
"description": "The username parameter is injectable."
}
},
})Using the client as a context manager closes its HTTP session when the block finishes. You can also call client.close() yourself.
Available resources
Methods are grouped by resource on the client:
| Resource | What it covers |
|---|---|
client.reports | Create, search, update, import, export, and delete reports |
client.findings | List, add, update, reorder, and delete report findings |
client.companies | Companies, contacts, and client portal access |
client.templates | Report templates, revisions, content, and previews |
client.vulnerability_templates | Vulnerability templates and report template links |
client.vulnerabilities | The vulnerability library |
client.snapshots | Report review snapshots and diffs |
client.comments | Review comments, annotations, and resolution status |
client.notes | Report notes and pinning |
client.images | Image uploads and associations |
client.attachments | File uploads and associations |
client.planning | Calendar events, availability, users, and conflict checks |
client.ai | Vulnerability generation, translation, content improvement, and screenshot analysis |
client.api_keys | The current key and its effective permissions |
The API reference documents the request and response fields used by these methods.
Pagination
List methods accept page and limit when the endpoint supports pagination:
result = client.reports.list(page=1, limit=25)For scripts that need every item, use an iterator. Pages are fetched as they are needed:
for report in client.reports.iter(limit=100):
print(report["title"])Iterators are available for reports, companies, report templates, vulnerability templates, vulnerabilities, and planning events.
Structured template authoring
The structured authoring API edits report templates without replacing opaque builder HTML. Start by reading the server's schema and the current authoring document:
schema = client.templates.authoring_schema()
current = client.templates.authoring_document(template_id)
version = current["contentVersion"]The schema endpoint is the authority for supported elements, operations, enums, limits, canonical Liquid fields, and the finding-page loop pattern on that Vulnotes instance. Construct operations with the typed builders in vulnotes.authoring; these builders reject unknown fields and invalid combinations before a request is sent.
Dry-run the exact batch, then apply it with the same version and operations:
checked = client.templates.validate_operations(template_id, version, operations)
if not checked["valid"]:
for issue in checked["issues"]:
print(issue["path"], issue["message"])
else:
saved = client.templates.apply_operations(template_id, version, operations)An apply is atomic: either every operation succeeds and a revision is recorded, or none of them are saved. A batch contains 1 to 100 operations and is limited to 2 MiB.
contentVersion provides optimistic concurrency. If another writer saves after your read, apply returns HTTP 409 and the SDK raises TemplateVersionConflictError. Its expected_version and current_version attributes describe the conflict. Fetch the authoring document again, reconcile your intended changes, and validate a newly built batch. Never blindly retry stale operations.
Use client.templates.authoring_validation(template_id) to inspect validation issues in the currently stored document after migrations or imports.
Use exact Vulnotes Liquid fields such as client.name, report.title, report.executiveSummary, dates.endDate, scope.description, and stats.criticalCount. Finding detail pages use an explicit loop:
{% for vuln in vulnerabilities %}
<h2>{{ vuln.title }}</h2>
<div>{{ vuln.description }}</div>
{% unless forloop.last %}{% pagebreak %}{% endunless %}
{% endfor %}The API dry-run rejects invented roots, misspelled canonical fields, and undeclared custom variable keys.
Uploading and exporting files
Upload methods accept a path, bytes, an open binary file, or a (filename, content) tuple.
client.images.upload("evidence.png", report_id=report_id)
client.attachments.upload("nmap-scan.xml", report_id=report_id)Exports return bytes. Pass path to save the result at the same time:
client.reports.export_pdf(report_id, path="report.pdf")
client.reports.export_xlsx(
report_id,
finding_fields=["title", "severity"],
path="findings.xlsx",
)
client.reports.archived_pdf(report_id, path="final.pdf")archived_pdf() downloads the PDF stored when a report was completed. It raises NotFoundError if the report has no archived PDF.
Handling errors
Every SDK exception inherits from VulnotesError. HTTP errors use more specific classes, so callers can handle only the cases they care about.
from vulnotes import NotFoundError, PermissionDeniedError, VulnotesClient
client = VulnotesClient()
try:
report = client.reports.get(report_id)
except NotFoundError:
print("The report does not exist or is not visible to this key")
except PermissionDeniedError as error:
print(f"The API key is missing a permission: {error.message}")Status errors include status_code, message, body, and the underlying response. Connection and timeout failures use APIConnectionError and APITimeoutError.
Idempotent requests are retried after connection failures and HTTP 429, 502, 503, or 504 responses. The defaults can be changed when creating the client:
client = VulnotesClient(
timeout=60,
max_retries=5,
)Set verify_ssl=False only for a local or lab instance with a self-signed certificate.
Calling an endpoint directly
If a new API endpoint is not wrapped by the installed SDK version, call it through the client. Authentication, error handling, timeouts, and response parsing still apply.
snapshots = client.request(
"GET",
f"/reports/{report_id}/snapshots",
)The SDK source and release history are available in the vulnotes-python-sdk repository.
