Incident History#
Every incident carries a history: a time-ordered list of comments from your organisation, updates from PhishFort analysts, and automated status lines. Each entry tells you who wrote it through authorDetails, a structured object you can filter on directly, so you can tell client messages from PhishFort updates, and automated lines from human ones, without parsing any text.
If you are building a new integration, build on authorDetails. Integrations written against the older author string and type field keep working; see Legacy structure and Migrating an Existing Integration.
Enabled per account
authorDetails is switched on per account. Contact PhishFort support to enable it for yours before you go live. Until it is enabled, history entries carry only the legacy fields, so treat authorDetails as optional in your parser.
Where it appears#
authorDetails is returned on each history entry from the single-incident endpoints:
| Method | Endpoint |
|---|---|
| GET | /v1/incident/{id} |
| GET | /v1/incident/subject/{subject} |
It needs no extra request parameters. When it's enabled for your account, it's present on every history entry those endpoints return.
Structure#
{
"id": string,
"timestamp": Date,
"message": string,
"authorDetails": {
"type": "client" | "phishfort",
"clientId": string | null,
"email": string | null,
"automated": boolean
},
"author": string, // display string, e.g. "PhishFort System" or "Client (jane@acme.com)"
"type": "info" | "info/auto" | "info/comment" // legacy classification, see below
}
| Field | Type | Meaning |
|---|---|---|
id |
string |
Stable identifier for the entry. Use it to de-duplicate entries across polls. |
timestamp |
Date |
When the entry was recorded, ISO 8601 in UTC. |
message |
string |
The comment, update, or status line. |
authorDetails.type |
"client" | "phishfort" |
client for messages and takedown approvals or declines from your organisation. phishfort for everything else, including analyst updates and automated status lines. |
authorDetails.clientId |
string | null |
The id of the client that wrote the entry, when it can be matched to exactly one of the clients your API key covers. Always null for phishfort entries. |
authorDetails.email |
string | null |
The responder's email address, when the entry was recorded with one (for example a comment posted with an author email, or a takedown approval). Always null for phishfort entries. |
authorDetails.automated |
boolean |
true for automated, system-generated entries such as Awaiting client approval or Takedown initiated. Always false for client entries. |
author |
string |
Human-readable author, suitable for display. Do not parse it; use authorDetails instead. |
type |
string |
Legacy classification. See Legacy structure. |
Some entries carry extra fields for specific events, such as the reopen object on a re-open or takedown review request. See Re-open and takedown review requests.
Response Example#
{
"data": {
"id": "054zKkjCnR1I3B3U812z",
"history": [
{
"id": "5521874",
"timestamp": "2026-09-10T08:12:04.000Z",
"message": "Awaiting client approval",
"authorDetails": {
"type": "phishfort",
"clientId": null,
"email": null,
"automated": true
},
"author": "PhishFort System",
"type": "info/auto"
},
{
"id": "5521908",
"timestamp": "2026-09-10T09:30:47.000Z",
"message": "Takedown approved by jane@acme.com",
"authorDetails": {
"type": "client",
"clientId": null,
"email": "jane@acme.com",
"automated": false
},
"author": "Client (jane@acme.com)",
"type": "info"
},
{
"id": "5521911",
"timestamp": "2026-09-10T09:31:02.000Z",
"message": "Please prioritise this one, it is being shared with our customers.",
"authorDetails": {
"type": "client",
"clientId": "a1B2c3D4e5F6g7H8i9J0",
"email": null,
"automated": false
},
"author": "Client (Acme Corp)",
"type": "info/comment"
},
{
"id": "5522046",
"timestamp": "2026-09-10T11:05:19.000Z",
"message": "We've contacted the hosting provider and are waiting on a response.",
"authorDetails": {
"type": "phishfort",
"clientId": null,
"email": null,
"automated": false
},
"author": "PhishFort",
"type": "info"
}
]
}
}
Other incident fields are left out above for brevity. See DetailedIncidentStructure for the full response.
Usage Examples#
import requests
incident_id = "054zKkjCnR1I3B3U812z"
response = requests.get(
f"https://capi.phishfort.com/v1/incident/{incident_id}",
headers={"accept": "application/json", "x-api-key": "YOUR_API_KEY"},
)
history = response.json()["data"].get("history", [])
# authorDetails is optional: skip entries without it
detailed = [e for e in history if "authorDetails" in e]
client_entries = [e for e in detailed if e["authorDetails"]["type"] == "client"]
analyst_updates = [
e for e in detailed
if e["authorDetails"]["type"] == "phishfort" and not e["authorDetails"]["automated"]
]
const incidentId = "054zKkjCnR1I3B3U812z";
const response = await fetch(
`https://capi.phishfort.com/v1/incident/${incidentId}`,
{ headers: { accept: "application/json", "x-api-key": "YOUR_API_KEY" } }
);
const { data } = await response.json();
// authorDetails is optional: skip entries without it
const detailed = (data.history ?? []).filter((e) => e.authorDetails);
const clientEntries = detailed.filter((e) => e.authorDetails.type === "client");
const analystUpdates = detailed.filter(
(e) => e.authorDetails.type === "phishfort" && !e.authorDetails.automated
);
Legacy structure#
Before authorDetails, the only way to classify a history entry was to match the author display string or the type field. Both are still returned on every entry, so integrations built this way keep working.
Supported, but plan to move
The legacy classification is still supported. It will be deprecated in the future, and we will give you plenty of notice before anything changes. We encourage you to switch to authorDetails whenever you can; the migration steps below show how to do it without a cutover.
author#
author |
Meaning |
|---|---|
PhishFort System |
Automated status lines, e.g. Awaiting client approval, Takedown initiated, Approval received - takedown proceeding. |
PhishFort |
Messages and updates from PhishFort analysts. |
Client (<sender>) |
Messages from your organisation (via the dashboard or POST /incident/{id}/comment), and takedown approvals or declines. <sender> is the responder's email or display name; when none is stored the value is Client. |
type#
type |
Meaning |
|---|---|
info/auto |
Automated status line. |
info |
Analyst update, or a takedown approval or decline from your organisation. |
info/comment |
Comment from your organisation. |
How the two line up#
type |
author |
authorDetails.type |
authorDetails.automated |
|---|---|---|---|
info/auto |
PhishFort System |
phishfort |
true |
info |
PhishFort |
phishfort |
false |
info/comment |
Client (<sender>) or Client |
client |
false |
info |
Client (<sender>) or Client (takedown approvals and declines) |
client |
false |
Migrating an Existing Integration#
If your integration already classifies history entries by matching the author string or checking type, you can move it to authorDetails without a cutover date. author and type keep their current values, so nothing breaks while you switch.
1. Map your existing checks#
| If you check today | Use instead |
|---|---|
author == "PhishFort System" or type == "info/auto" |
authorDetails.type == "phishfort" and authorDetails.automated == true |
author == "PhishFort" |
authorDetails.type == "phishfort" and authorDetails.automated == false |
author starts with Client |
authorDetails.type == "client" |
The email inside Client (<sender>) |
authorDetails.email |
| Which of your clients wrote the entry | authorDetails.clientId (new, no string equivalent) |
Two checks don't have a direct authorDetails equivalent:
- Comments vs. takedown responses. Both are
cliententries. If you need to tell them apart, keep usingtype(info/commentfor comments) or themessagetext (Takedown approved by …,Takedown declined by …). - Display names. When
Client (<sender>)holds a display name rather than an email,authorDetails.emailisnull. If you show the sender to your users, keep showingauthor.
2. Accept the field in your parser#
Add authorDetails as an optional field on your history entry model. If your client rejects unknown fields (strict schemas, generated SDKs with additionalProperties: false), update it before the field is enabled for your account.
3. Read authorDetails first, fall back to author#
Wrap the classification in one helper that prefers authorDetails and falls back to the old string checks when it's missing. The same code then works before and after the field is enabled for your account.
def classify(entry):
"""Returns (kind, automated): kind is "client" or "phishfort"."""
details = entry.get("authorDetails")
if details:
return details["type"], details["automated"]
# Fallback for entries without authorDetails
author = entry.get("author", "")
if author == "PhishFort System":
return "phishfort", True
if author.startswith("Client"):
return "client", False
return "phishfort", False
// Returns { kind: "client" | "phishfort", automated: boolean }
function classify(entry) {
if (entry.authorDetails) {
return {
kind: entry.authorDetails.type,
automated: entry.authorDetails.automated,
};
}
// Fallback for entries without authorDetails
const author = entry.author ?? "";
if (author === "PhishFort System") return { kind: "phishfort", automated: true };
if (author.startsWith("Client")) return { kind: "client", automated: false };
return { kind: "phishfort", automated: false };
}
4. Ask PhishFort support to enable it#
Once your integration handles the field, contact PhishFort support to switch authorDetails on for your account. Then call GET /v1/incident/{id} for an incident with a known history and confirm every entry includes authorDetails and that your classification is unchanged.
5. Remove the fallback (optional)#
After it's enabled, every history entry from the single-incident endpoints carries authorDetails, so you can drop the string checks. Keeping the fallback is harmless, and it protects you if you use the same code against an account that doesn't have the field enabled.
Notes#
authorandtypeare unchanged.authorDetailsis added alongside them, so existing integrations keep working.- Don't rely on
clientIdoremailbeing set oncliententries. Each one is filled in only when the entry carries that information. - For how history entries fit into the incident response, see IncidentHistory.