This guide explains how the pieces fit together, then walks you through reporting your first finding.
This is the companion to the API reference at https://api.defense.com/docs, which lists every field and rule. Read this first to understand the model; use the reference for the exact contract.
How it fits together
Three key concepts to be aware of:
Source - where your findings come from: one pen test, one scan report, one feed. It's the bucket everything groups under. You never create a source directly, creating a threat creates the source for you the first time, then reuses it.
Finding - one issue reported by that source (e.g. "SQL injection in the login form"). Every finding maps 1:1 to a threat in the portal. These are what we mean by "the source's findings".
Threat - what your team or customers teams actually triages and remediates in Defense.com.
You don't create the source, the finding and the threat in separate steps. A single call creates all three.
Files attach at two different levels, and this is the second thing people trip on:
Report-level documents attach to the source - the whole pen-test PDF, a letter of completion, a raw scanner export.
Finding-level evidence attaches to a single finding - the screenshot that proves this SQL injection.
Source (the bucket — created for you on your first call)
│
├── Finding ──▶ Threat + evidence (screenshots, per finding)
├── Finding ──▶ Threat + evidence
│
└── documents (report-level: the PDF, letter of completion, …)
Before you start
Get a Write API key. In the portal go to Account → API Keys, create a key with the Write scope, set an expiry, and copy it — it is shown only once.
Base URL is
https://api.defense.com. Every path below is relative to it.Authenticate every request with the key as a bearer token:
Authorization: Bearer YOUR_API_KEY
Partners have a matching variant of each endpoint that acts on a company you manage — same body, path prefixed with
/v1/partner/companies/{company_id}. See Partner endpoints below.
Not to be confused with the MCP integration, which authenticates with a defense-api-key header — that is a separate product. The REST API described here uses the bearer token above.
Step 1 - Report your first finding
One POST /v1/threats creates the source, the finding and the threat together.
POST /v1/threats
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"title": "SQL injection in login form",
"description": "User input is concatenated into the query.",
"risk_score": "high",
"source": {
"category": "pen_test",
"name": "Acme Q2 2026 Pen Test",
"external_reference": "ACME-PT-2026-Q2"
},
"finding": {
"external_reference": "ACME-PT-2026-Q2-001"
},
"remediations": [
{ "title": "Parameterise queries", "description": "Use bound parameters." }
],
"affected_assets": [
{ "hostname": "app01.acme.example.com" }
]
}
Response — 201 Created (trimmed):
{
"id": "ca0f3108-...", // the threat
"risk_score": "high",
"source": {
"id": "1a389eb9-...", // ← this is your external_source_id
"category": "pen_test",
"name": "Acme Q2 2026 Pen Test",
"external_reference": "ACME-PT-2026-Q2"
},
"finding": {
"id": "ef45399d-...",
"external_reference": "ACME-PT-2026-Q2-001",
"uploads": []
}
}
The one thing to remember: source.id in this response is what other endpoints call external_source_id. It is the source's id, not the threat's — and this create response is the only place it comes back (the read endpoints don't include it). Save it. Lost it? Re-post the same source + finding; the 200 OK response returns it again.
Required in the body: title, description, risk_score (info, low, medium, high or critical), a source, a finding, at least one remediation, and at least one affected_asset. Full field rules live in the API reference; the ones that catch people out are under Restrictions below.
Step 2 - Add more to the same source
Don't create a new source per finding. Send the same source.category + source.external_reference and Defense.com reuses the existing source; give each finding its own finding.external_reference.
Re-posting is safe, and it does one of two things:
New
finding.external_reference→ a new finding and threat — 201 Created.Same
finding.external_reference→ the existing threat gains another detection instead of a duplicate — 200 OK. This is how you add assets beyond the 25-per-request limit, or attach more evidence later.
Step 3 - Attach files
Two mechanisms, two levels. Choose by what the file is about.
Evidence for one finding — inline, no extra call
Nest base64-encoded files in finding.uploads on the same POST /v1/threats:
"finding": {
"external_reference": "ACME-PT-2026-Q2-001",
"uploads": [
{ "filename": "payload.png", "file_data": "<base64>", "caption": "Injected payload returning rows" }
]
}Images and PDFs only — PNG, JPG, GIF, PDF — up to 5 MB each, 10 per finding, 10 MB total per request.
To add more later, re-post the same
finding.external_referencewith the extra uploads.
A report-level document — on the source
Use the source id from Step 1:
POST /v1/external_sources/{external_source_id}/documents
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{ "filename": "acme-q2-report.pdf", "file_data": "<base64>", "caption": "Full report" }
One document per call, 10 per source, up to 15 MB each.
Accepts PDF, Word (.docx), Excel (.xlsx), JSON, XML, CSV, plain text, YAML, PNG, JPEG. Not legacy
.doc/.xls/.ppt, archives or HTML.Re-sending identical bytes returns the existing document (200 OK), so retries are safe.
Give text-based files an extension: CSV, JSON, XML, YAML and plain text carry no format signature, so they're identified by the
filenameextension — include it (notes.csv,export.json) or the file is rejected as an unknown type. Binary formats (PDF, images, Office) identify themselves.
Which one? Evidence for a specific issue → a finding upload. The whole report, or anything about the engagement overall → a source document.
Step 4 — See it in the portal
Findings appear on their source's page, and an External filter on the Threats list shows everything that arrived this way. Report documents render on the source; finding evidence renders on the finding (images as thumbnails you can open full-size).
Partner endpoints
Acting on a company you manage? Use the partner variant — identical body, company chosen by the path:
Action | Your own company | A managed company (partner) |
Report a finding |
|
|
Attach a document |
|
|
Requires a partner API key.
affected_assetsandaffected_usersresolve against the managed company, not yours.A company you don't manage - or a source that isn't theirs - returns 404.
The partner surface is create-only; reads and updates are own-company.
Restrictions worth knowing up front
Affected assets (affected_assets)
At least one is required; at most 25 per request. More than 25 for one finding? Re-post the same finding (Step 2) and they aggregate.
Each entry needs at least one identifying field:
hostname,ip_address,app_name,email,ssidorurl. Other keys are ignored.ip_addressmust be public - private, loopback and link-local addresses are rejected.urlmust be an absolute http/https URL.Address ranges (CIDR, from/to) are rejected — one entry identifies one asset.
Affected users (affected_users, optional) — a list of email addresses that must already exist on the account; max 25.
Rate limits - 1,000 threat creations/day and 1,000 document uploads/day per company, plus a global 500 requests / 5 minutes across the API.
When something is rejected
Status | Meaning |
| Missing or invalid API key. |
| Wrong scope (you need Write), or a partner call made with a non-partner key. |
| Source or company not found, or not yours. |
| The body broke a rule — the message says which. |
Framework errors return { "error": "..." }; validation errors return { "errors": "..." }.
For the full field-by-field contract, see the API reference: https://api.defense.com/docs
