---
title: "Export a campaign as JSON, CSV, or TXT"
method: GET
path: "/api/v1/campaigns/{id}/export"
---

# Export a campaign as JSON, CSV, or TXT

`GET /api/v1/campaigns/{id}/export`

## Request

- Base URL: `https://phishunt.io`
- URL: `https://phishunt.io/api/v1/campaigns/{id}/export`
- Auth: none declared

Same data as `GET /api/v1/campaigns/{id}`, served as a downloadable
attachment. `Content-Disposition` filenames use only the resolved
campaign key, never a domain and never the raw identifier supplied by
the caller.

For an archived key (see `GET /api/v1/campaigns/{id}`), only
`format=json` has an honest answer - it serves the same
`ArchivedCampaign` object as an attachment. `format=csv` and
`format=txt` stay 404 for an archived key: both promise live
enrichment (URL, status, relationship score) that an archived
snapshot's last-known member set does not retain, and a silently
thinner export under the same content-type contract would be worse
than an honest 404.

## Path parameters

- `id` string, required

## Query parameters

- `format` 'json' | 'csv' | 'txt'

## Response `200`

Campaign data as an attachment. CSV columns are `uuid,domain,url,company,status,first_seen,relationship_score,evidence_types,registrable_domain` (semicolon-joined signal ids for `evidence_types`; `registrable_domain` is appended last so existing column offsets never shift); TXT is one member URL per line. `format=json` on an archived key returns the ArchivedCampaign shape instead.

- union
  - Campaign: Full campaign object - the CampaignSummary fields plus an evidence breakdown and every member. Shared-infrastructure grouping of public detections, not an attribution claim. This is the LIVE shape (`state` is always "live" here); a key that is no longer live but whose history is retained returns the different, thinner ArchivedCampaign shape instead (still HTTP 200) - see `GET /api/v1/campaigns/{id}`.
    - `state` 'live': Always "live" on this shape - see ArchivedCampaign for the non-live variant.
    - `data_status` 'ok' | 'stale' | 'missing': Same freshness signal as CampaignsResponse.data_status.
    - `id` integer: Deprecated - versioned per daily rebuild (the same campaign can have many different ids over its lifetime), kept only for backward compatibility. Use `key` for permalinks or anything that needs to survive a rebuild.
    - `key` string: Stable campaign identifier; use this for permalinks.
    - `size` integer: Number of distinct registrable domains (Public Suffix List, private section included) in this cluster. Sibling subdomains of one domain count once; each platform tenant counts once.
    - `host_count` integer: Number of hostnames (members.length).
    - `active_count` integer: Number of hostnames that are currently active detections.
    - `brands` string[]
    - `confidence` 'possible campaign' | 'suspected cluster': The two words this project ever uses for a cluster's confidence. Derived from `confidence_score`: >= 0.70 is "possible campaign", >= 0.40 is "suspected cluster" (below 0.40, "weak", is never surfaced by this API).
    - `confidence_score` number, float: Raw campaign_confidence float (0-1) this cluster's `confidence` label is thresholded from. Formula: clamp(0.45*D + 0.25*V + 0.20*T + 0.10*B, 0, 1) * S - see `confidence_metrics` for the D/V/T/B/S breakdown.
    - `confidence_metrics` object: The confidence formula's per-component breakdown (each 0-1). D = link strength, V = evidence diversity, T = time compactness, B = strong-link share, S = size discount multiplier. Can be an empty object for a cluster with no internal edges yet.
      - `D` number, float
      - `V` number, float
      - `T` number, float
      - `B` number, float
      - `S` number, float
    - `first_seen` string, date-time, nullable
    - `last_activity` string, date-time, nullable
    - `top_evidence` object, nullable
      - `type` string
      - `coverage` string
    - `url` string: Link to the /campaigns/{key}/ page for this cluster.
    - `algorithm_version` string, nullable
    - `generated_at` string, date-time, nullable
    - `evidence_summary` CampaignEvidenceSummaryItem[]
      - `type` string: Stable evidence-signal identifier.
      - `value` string, nullable: Display value for this signal (e.g. the shared cert fingerprint, IP, or registrar name). Not a claim about intent.
      - `members_matching` integer: Distinct members touched by an internal edge carrying this signal with a nonzero contribution.
    - `first_tracked` string, date-time, nullable: When this stable key's lineage was first tracked (oldest `valid_from` across every cluster_id this key has ever carried).
    - `history` object[]: Every transition (created/grew/shrank) this stable key's lineage has gone through, oldest first. Rebuilt daily; retained for 180 days.
      - `at` string, date-time
      - `event` 'created' | 'grew' | 'shrank'
      - `size` integer
      - `joined` string[]: Domains that joined at this transition.
      - `left` string[]: Domains that left at this transition.
    - `relationships` Relationship[]: Per-pair evidence drill-down: one entry per FORMATION internal edge (the edges that actually caused these members to be grouped together, as opposed to a merely-related or same-kit edge below the clustering threshold), sorted by score descending and capped at 50 pairs - see `relationships_truncated`.
      - `domain_a` string
      - `domain_b` string
      - `detail_url_a` string, nullable: Link to domain_a's detail page. Null when domain_a no longer has a live indicator row (pruned by retention).
      - `detail_url_b` string, nullable: Link to domain_b's detail page. Null when domain_b no longer has a live indicator row (pruned by retention).
      - `score` integer: This edge's persisted relationship score.
      - `tier` string: Persisted score tier for this edge (e.g. "high"/"medium"/"low").
      - `demotion_reason` string, nullable: When set, the engine demoted this edge's display tier after scoring (e.g. failed_high_gate, same_kit_cap) - a display-only adjustment; it does not undo formation.
      - `first_reproduced` string, date, nullable: Date part of this edge's `valid_from` - when this specific link was first recorded.
      - `evidence` RelationshipEvidence[]: This pair's evidence rows with a positive contribution, sorted by `contributed` descending, capped at 12. Rows whose signal has no fixed display label are omitted.
        - `label` string: Fixed, human-readable label for this signal (e.g. "same IP", "same TLS cert"). Never a de-underscored raw signal id.
        - `signal` string: Stable evidence-signal identifier, e.g. `same_ip`, `cert_fingerprint_exact`.
        - `category` string, nullable: The engine's evidence category for this signal.
        - `contributed` number, nullable: Points this row contributed toward the edge's `score`. <= 0 rows only ever appear in `discounted`.
        - `idf_factor` number, float, nullable: Inverse-document-frequency discount applied to this signal's base weight - a value shared by many sites scores lower than one shared by few.
        - `dampened_by` string, nullable: When set, the reason this row's contribution was reduced (e.g. hub_cardinality_cap, correlated-signal dampening).
      - `discounted` RelationshipEvidence[]: Evidence rows the engine dampened to zero or below (e.g. hub_cardinality_cap, correlated-signal dampening) - contributed nothing to `score`, kept separately here ("what did NOT count and why") rather than dropped. Sorted by `contributed` descending (least discounted first), capped at 6.
        - `label` string: Fixed, human-readable label for this signal (e.g. "same IP", "same TLS cert"). Never a de-underscored raw signal id.
        - `signal` string: Stable evidence-signal identifier, e.g. `same_ip`, `cert_fingerprint_exact`.
        - `category` string, nullable: The engine's evidence category for this signal.
        - `contributed` number, nullable: Points this row contributed toward the edge's `score`. <= 0 rows only ever appear in `discounted`.
        - `idf_factor` number, float, nullable: Inverse-document-frequency discount applied to this signal's base weight - a value shared by many sites scores lower than one shared by few.
        - `dampened_by` string, nullable: When set, the reason this row's contribution was reduced (e.g. hub_cardinality_cap, correlated-signal dampening).
    - `relationships_truncated` boolean: True when this cluster has more than 50 formation-edge pairs and `relationships` was cut to the 50 strongest.
    - `members` CampaignMember[]
      - `uuid` string, uuid
      - `domain` string
      - `registrable_domain` string: This member's registrable domain (Public Suffix List, private section included). Equals `domain` for a member that is its own apex; several members can share the same `registrable_domain` (see the campaign's `domains` grouping).
      - `url` string, nullable: Null when the member has been pruned from the live dataset (retention) but is still listed for completeness.
      - `company` string: Targeted brand slug.
      - `status` 'active' | 'offline'
      - `first_seen` string, date-time, nullable
      - `relationship_score` integer, nullable: Best (highest) score among this cluster's internal edges touching this member. Null when the member has no internal edge.
      - `relationship_tier` string, nullable: Score tier of the same best internal edge relationship_score is drawn from (e.g. "high"/"medium"/"low"). Null under the same condition as relationship_score.
      - `detail_url` string, nullable
      - `image` string, nullable: Screenshot path, relative to the static root. Coverage is not guaranteed for every member.
      - `phishunt_score` integer, nullable: This member's own detection score (0-100) from the 5-layer detector - unrelated to relationship_score.
      - `ip` string, nullable
      - `asn` string, nullable
      - `org` string, nullable: Hosting organization.
      - `country` string, nullable: Hosting country.
      - `registrar` string, nullable
      - `cert` string, nullable: TLS certificate issuer.
      - `gsb_category` string, nullable: Google Safe Browsing threat category, when flagged.
      - `malicious_google` string, nullable: "1" or "0" - unlike the equivalent field on the Site schema (`/api/v1/domains`, `/feed.json`), this is NOT cast to a real JSON boolean here; it is passed straight through from the underlying text column. An empty string means that source was never asked (urlscan timeout or quota). Null when the member has no live row.
      - `malicious_openphish` string, nullable: Same "1"/"0" string convention as malicious_google - see its note.
      - `malicious_phishtank` string, nullable: Same "1"/"0" string convention as malicious_google - see its note.
      - `malicious_tweetfeed` string, nullable: Same "1"/"0" string convention as malicious_google - see its note.
      - `malicious_urlscan` string, nullable: Same "1"/"0" string convention as malicious_google - see its note.
    - `domains` CampaignDomainGroup[]: One entry per distinct registrable domain in this cluster, sorted by domain. Sibling hostnames under one domain (or one platform tenant) are grouped into a single entry here.
      - `domain` string: The registrable domain (Public Suffix List, private section included).
      - `host_count` integer: Number of hostnames under this domain.
      - `active_count` integer: Number of those hostnames that are currently active detections.
      - `hosts` string[]: Every hostname grouped under this domain.
      - `uuids` string[]: uuid of each hostname in `hosts` that still has a live indicator row - can be shorter than `hosts` when some do not.
  - ArchivedCampaign: Returned by GET /api/v1/campaigns/{id} (HTTP 200) for a key that is no longer live but whose history the correlation sidecar still retains (180-day retention). Deliberately thinner than Campaign: no evidence_summary, and members is the last recorded snapshot only (no live Site enrichment - url/status/relationship_score), since an archived cluster's indicators are not guaranteed to still be in the active feed. Shared-infrastructure grouping of public detections, not an attribution claim.
    - `state` 'archived'
    - `data_status` 'ok' | 'stale' | 'missing': Same freshness signal as CampaignsResponse.data_status.
    - `key` string: Stable campaign identifier.
    - `label` 'possible campaign' | 'suspected cluster': The two words this project ever uses for a cluster's confidence, from its last recorded state.
    - `confidence_score` number, float: Raw campaign_confidence from the last recorded state (0-1).
    - `size` integer: Number of distinct registrable domains (Public Suffix List, private section included) in this cluster. Sibling subdomains of one domain count once; each platform tenant counts once. Archived rows built before corr-1.3.0 recorded hostnames.
    - `host_count` integer, nullable: Number of hostnames (members.length) in the last recorded state. Null for a row archived before corr-1.3.0, whose summary_json never recorded a separate hostname count.
    - `first_tracked` string, date-time, nullable
    - `last_seen` string, date-time, nullable: When this lineage was last live.
    - `end_state` 'dissolved' | 'merged' | 'split' | 'unknown': 'dissolved' - members dropped out of the active feed and nothing continues this identity. 'merged' - absorbed into another cluster; see `successors`. 'split' - this cluster's members were distributed into multiple new clusters; see `successors`. 'unknown' - no transition record was retained (e.g. a sidecar predating this classification).
    - `successors` string[]: Public key(s) this lineage continues under, for end_state merged/split. A successor that is itself weak, suppressed, or unresolvable is dropped rather than shown (fail closed) - this can be an empty array even for a merged/split end_state.
    - `history` object[]
      - `at` string, date-time
      - `event` 'created' | 'grew' | 'shrank' | 'dissolved' | 'merged' | 'split' | 'unknown'
      - `size` integer
      - `joined` string[]
      - `left` string[]
    - `members` object[]
      - `domain` string
      - `uuid` string, uuid
      - `company` string
    - `url` string: Link to the /campaigns/{key}/ page for this cluster.

## Other responses

- `400`: Bad request - unknown `format` value.
- `404`: Unknown campaign id - not found, not live, or a low-confidence "weak" cluster (never surfaced).
- `429`: Rate limited (more than 20 requests in 10 seconds from one IP). Retry after 10 seconds.

---

[API](https://skmtc.dev/phishunt/apis/phishunt-api.md) · [All operations](https://skmtc.dev/phishunt/apis/phishunt-api/llms.txt) · [OpenAPI document](https://skmtc.dev/phishunt/apis/phishunt-api/revisions/0e07c255f282?raw)
