Skip to content

Data Structures#

Contained here is a list of example structures you can expect to see on various API responses.

IncidentStructure#

The incident structure represents the simplest incident structure returned by the API.

{
    "id": string,
    "clientId"?: string,
    "safeDomain": string,
    "subject"?: string, // resolved display name: URL, domain, email, phone, or IP
    "incidentType": string, // see Incident Types below
    "domain"?: string, // only present on "domain" incidentType if "url" path differs from domain
    "url"?: string, // only present if URL differs from domain or if incidentType is social
    "source": string,
    "status": string, // see Status Values below
    "timestamp": Date,
    "lastHistoryUpdateTimestamp"?: Date, // date of the most recent status change
    "burnStartedTimestamp"?: Date, // date when takedown process started
    "takedownTimestamp"?: Date, // date when takedown concluded successfully
    "reportedBy"?: string, // email of the user who reported the incident
    "threatTaxonomy"?: ThreatTaxonomy, // threat classification (see below)
    "incidentClass": string // broad incident classification (see Incident Class below)
}

Info

Every incident will have either a url OR domain field present, in some cases both may be present. But it should be noted that either of them may be undefined depending on the domain name structure and incidentType.

Incident Types#

For CentralDB-backed incidents, incidentType is the CentralDB observable_type projected into the Unified Client API response. Response values and incidentType filters use the following vocabulary:

Value Observable
page Website or domain page.
email Email address.
social Social-media profile, page, post, or other social URL.
phone Phone number.
app Application or app-store listing.
browser_extension Browser extension or extension-store listing.
ip_address IP address.
other Observable that does not match another supported type.

Clients should tolerate new incidentType values so future observable types do not break response or webhook processing.

CentralDB normalises legacy source values before exposing them: domain becomes page, all app:* variants become app, browser:extension becomes browser_extension, and ipv4 becomes ip_address.

Status Values#

The status field returns a computed verbose status value. The possible values are:

Status Description
pending_review Incident has been reported but not yet reviewed by an analyst.
case_building Analyst is reviewing the incident, choosing a plan of action and gathering evidence.
approval_required Incident is in case building and the takedown is pending approval.
takedown_ready Incident is in case building and the takedown has been approved.
pre_weaponised Incident has been placed into monitoring.
takedown_in_progress Takedown process has been initiated but not yet completed.
takedown_success Takedown has been completed successfully.
takedown_attempt_failed Takedown was attempted but failed.
blocklisted Incident has been blocklisted (no takedown performed).
action_required Incident is awaiting action from the client.
closed Incident is resolved/closed.

DetailedIncidentStructure#

The detailed incident inherits all fields from IncidentStructure but appends additional fields including history, incidentState, registrar, hostingProvider, and blocklists:

{
    "id": string,
    "clientId": string,
    "safeDomain": string,
    "subject"?: string,
    "incidentType": string, // see Incident Types above
    "domain"?: string, // only present on "domain" incidentType
    "url"?: string, // only present if URL differs from domain
    "source": string,
    "status": string, // see Status Values above
    "timestamp": Date,
    "lastHistoryUpdateTimestamp"?: Date,
    "burnStartedTimestamp"?: Date,
    "takedownTimestamp"?: Date,
    "reportedBy"?: string,
    "threatTaxonomy"?: ThreatTaxonomy, // expanded format (see ThreatTaxonomy)
    "incidentClass": string,
    "history": IncidentHistory[],
    "incidentState": IncidentState,
    "registrar"?: Registrar, // registrar info with takedown difficulty
    "hostingProvider"?: string, // hosting provider name
    "blocklists": BlocklistListing[], // partner blocklist / warning-banner coverage, [] when none
    "insights"?: IncidentInsights // infrastructure signals from the latest snapshot
}

Info

Every incident will have either a url OR domain field present, in some cases both may be present. But it should be noted that either of them may be undefined depending on the domain name structure and incidentType.

Incident Class#

incidentClass is a broad, single-value summary of what an incident is. It is derived from the incident's threat classification, and takes one of the following values:

Value Meaning
phishing Credential, seed-phrase or wallet harvesting, and other attacks that trick a victim into handing over access.
malware Distribution of malicious software or malicious API endpoints.
scam Fraud that takes value directly from the victim — investment scams, fake offers, promotion scams, scam services.
infringement Unauthorised use of protected material — copyrighted content, counterfeits, software infringement, trademark advertising.
brandabuse Impersonation of a brand or person, fake affiliation, and related identity abuse.
other Everything else: content and passive observations that are not themselves an attack — parked pages, error pages, gambling or adult content, typosquats with no active use — and incidents that have not yet been classified.

Relationship to threatTaxonomy

incidentClass is a summary. threatTaxonomy carries the full classification and is the more precise field: on the detail endpoint it breaks the incident down by target, type and threat type. If you need detail beyond the six values above, read threatTaxonomy rather than incidentClass. An incident that has not been classified reads other and carries no threatTaxonomy, so the two together tell you whether other means "classified as unremarkable" or "not classified yet".

incidentClass is not the incident type

incidentClass describes the nature of the threat. incidentType describes the kind of observable listed under Incident Types. The two use different vocabularies and are never interchangeable. Treat an unknown incidentClass as unrecognised, while tolerating new incidentType values for forward compatibility.

ThreatTaxonomy#

The threat taxonomy classifies the type of threat an incident represents. This field is present when a threat classification has been assigned.

The list endpoint (GET /incidents) returns a lite format:

{
    "name": string, // taxonomy name (e.g. "Brand - Website - Impersonation")
    "description": string // additional description (may be empty)
}

The detail endpoint (GET /incident/{id}) returns an expanded format with additional classification fields:

{
    "name": string, // taxonomy name (e.g. "Brand - Website - Impersonation")
    "description": string, // additional description (may be empty)
    "incidentClass"?: string, // broad classification (see Incident Class below)
    "incidentTargetType"?: TypeReference, // target type classification
    "incidentType"?: TypeReference, // incident type classification
    "incidentThreatType"?: TypeReference // threat type classification
}

TypeReference#

Sub-objects within the expanded threat taxonomy use the following structure:

{
    "id": string,
    "name": string,
    "description": string
}

Registrar#

The registrar object provides information about the domain registrar and its estimated takedown difficulty. This field is only present on the detail endpoint when registrar data is available.

{
    "name"?: string, // registrar name
    "difficulty"?: string, // takedown difficulty level
    "description"?: string // description of the difficulty level
}

IncidentState#

The incident state is a structure for reflecting the real-time state of an incident (whether or not it's responsive, has a warning banner, etc).

{
    "unresponsive": boolean, // the incident is not responding to requests
    "warningBanner": boolean, // a Google SafeBrowsing or Cloudflare warning banner is displayed
    "responsive": boolean, // the incident is responding to requests
    "contentRemoved": boolean, // the malicious content on the page has been removed
    "redirect": boolean,
    "inconclusive": boolean, // the state could not be determined
    "timestamp": Date // the timestamp of the last state scan
}

BlocklistListing#

While a takedown runs its course, PhishFort also submits the malicious URL or domain to security partners that can protect users immediately — browser warning pages and threat-intelligence blocklists. Each entry in the blocklists array records the coverage with one partner. The field is only present on the detail endpoint and is an empty array when nothing has been submitted.

{
    "authority": string, // partner name: "Google Safe Browsing", "Cloudflare", "VirusTotal", "Spamhaus"
    "type": "blocklist" | "warning-banner", // what the listing does (see below)
    "status": "listed" | "submitted", // whether the listing is confirmed live with the partner
    "listedAt": Date | null, // when the listing went live; null while status is "submitted"
    "link"?: string | null // public page showing the listing, when the partner provides one
}
type Meaning
warning-banner Users who try to visit the site see a full-page browser warning (e.g. Google Safe Browsing's red interstitial).
blocklist The URL or domain is listed in the partner's threat database, which security products consume to block or flag it.

Entries are ordered oldest listing first. A status of submitted means the submission has been made but the partner has not confirmed the listing as live yet — treat it as protection in progress, not failure. New partners may be added over time, so treat authority as an open set.

IncidentInsights#

Infrastructure signals for the incident, derived from the snapshots PhishFort captures over its lifetime. This field is only present on the detail endpoint, and only when at least one snapshot carries DNS, WHOIS or certificate data.

Snapshots are often partial, and older ones more so. Rather than stitching categories together from different points in time, the insights block reports the single most recent snapshot that carries any data, along with the timestamp it was taken at. Categories that snapshot did not capture are omitted.

{
    "snapshotTimestamp": Date | null, // when that snapshot was captured
    "dns"?: DnsInsights,
    "whois"?: WhoisInsights,
    "certificate"?: CertificateInsights,
    "hosting"?: HostingInsights, // not available on every incident, see below
    "page"?: PageInsights, // not available on every incident, see below
    "indicators"?: CaptureIndicators // not available on every incident, see below
}

Which categories you get varies by incident

dns, whois and certificate may appear on any incident. hosting, page and indicators come from a richer capture pipeline that does not run for every incident, and are absent otherwise.

Treat every category as optional and check for its presence. Do not assume a fixed shape, and do not treat an absent category as a negative finding — it means "not captured", not "nothing there".

Info

Raw, unprocessed snapshot blobs remain available from GET /incident/{incidentId}/snapshots, including the full history of captures. The insights block is a convenience layer over the latest one.

DnsInsights#

A summary of which DNS record types resolved, and how many answers each returned. Record types that resolved with no answers are omitted rather than reported as zero.

{
    "recordTypes": string[], // record types with at least one answer, sorted
    "recordCounts": { [recordType: string]: number } // answers per record type
}

WhoisInsights#

{
    "domain"?: string,
    "registrar"?: string,
    "registrarIANAId"?: string,
    "createdDate"?: Date, // domain registration date
    "updatedDate"?: Date,
    "expiryDate"?: Date,
    "domainAgeDays": number | null, // whole days since registration
    "isNewDomain": boolean | null, // true when registered less than 30 days ago
    "isExpired": boolean | null, // true when the registration lapsed
    "statuses"?: string[] // EPP status codes, e.g. "clientTransferProhibited"
}

A freshly registered domain is a strong signal of malicious intent — most phishing infrastructure is registered days before it is used — which is why isNewDomain is broken out separately from domainAgeDays.

Warning

domainAgeDays, isNewDomain and isExpired are null when the underlying WHOIS date was missing or could not be parsed. null means undetermined, not "no" — do not treat it as false.

CertificateInsights#

{
    "issuer"?: string, // certificate authority that issued it
    "subject"?: string,
    "validFrom"?: Date,
    "validTo"?: Date,
    "isValid": boolean | null, // true when the current time falls inside the validity window
    "subjectAltNames"?: string[] // other hostnames the certificate covers
}

subjectAltNames is often the most useful field here: a certificate covering several lookalike hostnames indicates infrastructure being prepared for more than the one URL in this incident.

Warning

isValid is null when either validity bound was missing, for the same reason as above — it means undetermined, not invalid.

HostingInsights#

Where the threat is actually served from. Only present on incidents that went through the richer capture pipeline.

{
    "cdnDetected": boolean, // the site sits behind a CDN or reverse proxy
    "cdnName"?: string, // the CDN, when it could be named
    "providers": HostingProvider[], // the serving infrastructure
    "originHints": HostingOrigin[] // infrastructure behind the CDN, when found
}

originHints is the operationally useful part. When a site is fronted by a CDN, the CDN is rarely the right recipient of a takedown notice — the origin behind it is. Each entry carries the abuse contacts matched to that provider.

Info

cdnDetected is true either when the provider is a recognised CDN by name, or when a distinct origin was found behind the serving IP — fronting by an unrecognised provider still counts.

HostingProvider#

{
    "ip": string,
    "network"?: string,
    "organization"?: string,
    "isCdn": boolean,
    "abuseEmails": string[] // may be empty
}

HostingOrigin#

{
    "ip": string,
    "network"?: string,
    "organization"?: string,
    "abuseEmails": string[] // may be empty
}

PageInsights#

What the captured page contained. Only present on incidents that went through the richer capture pipeline.

{
    "title"?: string,
    "metaDescription"?: string,
    "headings"?: string[], // capped at 20 entries
    "linkCount": number,
    "formInputCount": number,
    "imageCount": number
}

formInputCount is worth attention on a lookalike domain: input fields on an impersonating page indicate active credential or payment harvesting rather than a parked or placeholder site.

Info

The page's full visible text is not returned — it is unbounded in size. Use the snapshots endpoint if you need the raw capture.

CaptureIndicators#

Technical detail about the capture itself. Only present on incidents that went through the richer capture pipeline.

{
    "finalUrl"?: string, // where the capture ended up
    "requestedUrl"?: string, // only when it differs from finalUrl
    "redirectChain"?: string[],
    "ssdeepHash"?: string, // fuzzy content hash
    "pHash"?: string, // perceptual hash of the rendered screenshot
    "captureDurationMs"?: number,
    "captureError"?: string, // why the capture failed or the site was unreachable
    "scrapeRoute"?: ScrapeRoute
}

ssdeepHash and pHash let you correlate incidents that share content or appearance — useful for spotting one kit deployed across many domains.

ScrapeRoute#

How the capture was performed. Some sites cloak based on the visitor, so the route explains why a capture looks the way it does.

{
    "method"?: string,
    "uaType"?: string, // the user-agent class used
    "proxyType"?: string,
    "country"?: string, // exit country
    "exitIp"?: string,
    "carrier"?: string,
    "mobileExit"?: boolean
}

SnapshotStructure#

A point-in-time capture of an incident's infrastructure, returned by GET /incident/{incidentId}/snapshots.

{
    "id": string,
    "incidentId": string,
    "type": string | null, // what triggered the capture, e.g. "incident_created"
    "timestamp": Date | null, // when the capture was taken
    "description": string | null,
    "dns"?: object, // raw DNS records, keyed by record type
    "whois"?: object, // raw WHOIS fields
    "certificate"?: object // raw TLS certificate fields
}

Warning

dns, whois and certificate are passed through as captured — their internal keys come from the resolvers and parsers upstream, and every field inside them should be treated as optional. A category is omitted entirely when that capture recorded nothing for it.

For the same records with the signals already derived, use the insights block on the detail endpoint instead.

Rendered screenshots are not included; snapshots carry infrastructure records only.

IncidentHistory#

The incident history is a time-based entry of incident state change info.

{
    "timestamp": Date,
    "message": string,
    "author": string // who the entry is from — see below
}

author classifies the entry, so you can count approvals or filter customer messages without parsing message:

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/{incidentId}/comment), and takedown approvals or declines. <sender> is the responder's email or display name; when none is stored the value is Client.

Note

A takedown approval appears as a Client (<approver>) entry (Takedown approved by <approver>), followed by a PhishFort System entry Approval received - takedown proceeding once enforcement resumes.

UserStructure#

The user structure represents a dashboard user returned by the Users endpoint. Only client-safe fields are exposed.

{
    "id": string,
    "email": string,
    "name"?: string, // first name (may be null/absent)
    "surname"?: string, // last name (may be null/absent)
    "roles": string[], // see User Roles below
    "clientId"?: string, // the client the user belongs to (absent for staff/admins)
    "clientName"?: string, // display name of the user's client
    "mfaEnabled": boolean, // whether multi-factor authentication is enabled
    "createdAt": Date, // when the user was created
    "updatedAt": Date // when the user was last updated
}

User Roles#

The roles array on a UserStructure contains one or more of the following values:

Role Description
superuser Administrator for the client account (manage users, settings, and documents).
parentadmin Administrator across a parent/reseller account and its managed sub-clients.
user Standard dashboard user.
admin PhishFort-internal administrator role. Accepted as a filter value but internal users are excluded from client API responses.

PhishFort internal accounts are never returned

PhishFort staff accounts — identified by the internal admin role or a @phishfort.com email address — are excluded from this endpoint. Only your own account's users are returned.

ClientStructure#

The client structure represents a client account returned by the Clients endpoint.

{
    "id": string, // the client's unique ID
    "name": string, // the client's display name
    "active": boolean // whether the client account is active
}

UsageStructure#

The usage structure represents the remaining credit balance for a single client, returned by the Credit Usage endpoint.

{
    "clientId": string, // the client this balance is for
    "clientName": string | null, // the client's display name, if available
    "remainingCredits": number | "unlimited" | null // credits left, an unlimited allowance, or an unmetered client
}

remainingCredits is a number for a metered account, the string "unlimited" for an unlimited account, or null when the client is not using the credit system.

DocumentStructure#

The document structure represents a document on file, returned by the Documents endpoints. The single-document detail endpoint additionally includes a short-lived signedUrl.

{
    "id": string, // the document's unique ID
    "clientId": string, // the client the document belongs to
    "client": { // reference to the owning client (null if unavailable)
        "id": string,
        "name": string,
        "parentClientId": string | null
    } | null,
    "docType": string, // see Document Types below
    "fileName": string | null, // original file name
    "uploadedBy": { // who uploaded the document (null if unavailable)
        "id"?: string,
        "email"?: string,
        "name"?: string,
        "surname"?: string
    } | null,
    "createdAt": Date | null, // when the document was uploaded
    "updatedAt": Date | null, // when the document was last updated
    "signedUrl"?: string | null // temporary download URL (detail endpoint only)
}

Document Types#

The docType field on a DocumentStructure is one of the following values:

Type Description
loa Letter of Authorisation.
trademark_registration A trademark registration certificate or filing.
exec_loa An executive-specific Letter of Authorisation.
file A general supporting file.

PagingStructure#

The paging structure is returned by list endpoints that support pagination. It provides cursor-based pagination for efficient navigation through large result sets.

{
    "cursor": string, // current cursor position (typically the ID of the last item in the current page)
    "next": string | null, // cursor for the next page, null if no more results
    "limit": number, // number of items per page
    "count": number, // number of items in the current page
    "total": number // total number of items matching the query across all pages; only present when includeTotal=true is requested
}

Usage

  • Use the next value as the cursor query parameter in your subsequent request to fetch the next page
  • When next is null or absent, you have reached the end of the results
  • The default limit is 5000 if not specified in the request
  • Pass includeTotal=true to include the total field (supported on GET /incidents)