Skip to content

REST API Reference

PhishSpot exposes a REST API over JSON at https://platform.phishspot.com/api/v1. It covers essentially everything you can do in the admin app: build, schedule and analyze campaigns, manage contacts/groups/templates/courses/media/domains/autopilots, and stream results into your own tooling.

This chapter documents every endpoint in detail — parameters, request bodies, response fields and status codes — so you can integrate without reading the source. For a push-based event model see Chapter 26 Webhooks; for a natural-language AI interface over the same capabilities see Chapter 29 MCP Server.

Every authenticated request must send an API token as a bearer header:

Authorization: Bearer <token>

Get a token one of two ways:

From the admin UI (recommended). Account settings → API Tokens → New token (see Chapter 14). Copy the value — it is shown once. Store it in a secrets manager.

From the API. POST email + password (plus otp_attempt if 2FA is on) to /auth:

Terminal window
curl -X POST https://platform.phishspot.com/api/v1/auth \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"secret"}'
{ "token": "abc123…", "user": { "id": 2, "email": "admin@example.com" } }
FieldTypeDescription
emailstringRequired. The user’s email.
passwordstringRequired. The user’s password.
otp_attemptstringRequired only if the user has two-factor auth enabled.

A token belongs to a single user and inherits that user’s account memberships. Treat it like a password. All examples below assume $TOKEN holds a valid token.

  • Base URL: https://platform.phishspot.com/api/v1. All paths below are relative to it.
  • Content type: send Content-Type: application/json; bodies and responses are JSON.
  • Times: ISO-8601 (2026-05-20T14:22:33.000Z), UTC unless stated. The campaign scheduled_at input is interpreted in the account’s timezone.
  • IDs in paths: wherever a path takes :id, you may pass either the integer primary key (/campaigns/42) or the record’s prefixed id (/campaigns/camp_0u1k…). Responses always expose the integer id; some also expose the prefixed id.
  • account_id: nested routes take account_id in the path; it accepts the integer id or the acct_… prefixed id. Discover yours with GET /accounts.
  • Account scoping: a token can act only on accounts its user belongs to. Requesting a record in another account returns 404 — the API never confirms another tenant’s data exists.
  • Roles: read endpoints require any role (incl. member). Write endpoints (POST/PATCH/PUT/DELETE and state actions) require admin or editor; a member token gets 403. Team/billing and platform-domain admin actions require admin.
  • Pagination: endpoints that paginate accept ?page=N (1-based) and sometimes ?per_page=M or ?limit=M; defaults are noted per endpoint. Non-paginated lists return the full ordered set.

Unless an endpoint says otherwise, these apply to every call (only endpoint-specific codes are repeated below):

CodeBodyWhen
401 Unauthorized(empty)Missing or invalid Authorization token.
403 Forbidden{"error":"You are not authorized to perform this action"}Token valid but the user’s role is insufficient for this action.
404 Not Found{"error":"Resource not found"}No such record, or the record belongs to an account the token can’t access.
422 Unprocessable Content{"errors":{"field":["message"]}} (or {"errors":["message"]} for action endpoints)Validation failed; inspect errors.
429 Too Many Requests(varies)Rate limit exceeded; see §27.17. The Retry-After header says when to retry.

Returns the user behind the token.

Parameters: none (bearer token only).

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/me

Response 200 OK

FieldTypeDescription
idintegerUser id.
emailstringUser email.
namestringDisplay name.
localestringUI locale (en / pl).
accountsarrayAccounts the token can act on (see GET /accounts).

Lists every account the token’s user can access. Use it to find the account_id for nested routes.

Parameters: none.

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/accounts

Response 200 OK — array of:

FieldTypeDescription
idintegerAccount id (use in nested paths).
prefix_idstringPrefixed id (acct_…).
namestringAccount name.
localestringAccount default locale.
[{ "id": 11, "name": "Cydefen Tests", "locale": "pl", "prefix_id": "acct_3kf…" }]

Manage phishing-simulation campaigns: create and edit drafts, drive the campaign through its lifecycle (start, pause, stop, cancel), schedule a future send, duplicate, and read results, recipient progress, replies, and a per-contact event timeline.

Authorization is enforced per account: a campaign is only reachable if it belongs to one of the accounts the bearer token’s user is a member of (any membership role can read and write — there are no admin-only campaign actions). State-transition endpoints additionally require the campaign to be in a compatible state (e.g. you can only pause an in-progress campaign), returning 403 otherwise.

The campaign object emitted by show, create, update, and all state-transition endpoints is identical and described once under GET /campaigns/:id.

Lists every campaign in the account, newest first. Use it to enumerate campaigns before drilling into one. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer). Must be an account the token’s user belongs to.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/accounts/11/campaigns

Response 200 OK — JSON array of campaign objects (see GET /campaigns/:id for the per-object field list), ordered by created_at descending.

[
{
"id": 42,
"account_id": 11,
"name": "Q2 Invoice Lure",
"state": "in_progress",
"delivery_mode": "immediate",
"delivery_schedule": null,
"created_at": "2026-05-01T09:00:00.000Z",
"updated_at": "2026-05-02T14:12:00.000Z",
"email_subject": "Your April invoice is ready",
"email_content": "<p>Hello {{first_name}}…</p>",
"landing_html": "<form>…</form>",
"domain": "officelogin.in",
"course_id": 7,
"groups": [{ "id": 3, "name": "Finance" }],
"statistics": {
"total_contacts": 120,
"total_deliverables": 120,
"completion_percentage": 100.0
},
"can_start": false,
"can_pause": true,
"can_cancel": true
}
]

Status codes

CodeWhen
200Campaigns listed (empty array if the account has none).
403Token’s user is not authorized to view the account.
404account_id is not an account the token’s user belongs to.

Creates a new draft campaign in the account. All content fields are optional at creation — only name is required — so you can create a bare draft and fill it in with PATCH. Auth: Bearer; role: any role.

Parameters

All body params are wrapped in a campaign object.

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
campaign[name]bodystringyesCampaign name. Must be unique within the account (case-insensitive).
campaign[delivery_mode]bodystringnoOne of immediate, scheduled, staggered. Defaults to immediate.
campaign[delivery_schedule]bodystringnoFree-form schedule string used only when delivery_mode is scheduled (ISO8601 datetime, or 5-field cron). Prefer the /schedule endpoint instead.
campaign[email_subject]bodystringnoSubject line. May contain email merge tags (e.g. {{first_name}}); unknown tags fail validation.
campaign[email_content]bodystringnoHTML email body. Must be well-formed HTML and use only allowed email merge tags.
campaign[landing_html]bodystringnoLanding-page HTML. Must be well-formed HTML and use only allowed landing merge tags.
campaign[landing_css]bodystringnoLanding-page CSS. Must be well-formed CSS.
campaign[landing_page_enabled]bodybooleannoWhether the landing page is served. Defaults to false.
campaign[platform_domain_id]bodyintegernoId of the PlatformDomain (attacker domain) used for sending and landing. Required before the campaign can start.
campaign[course_id]bodyintegernoId of the e-learning course to redirect victims to (used when end_action_type is redirect_to_course).
campaign[from_email]bodystringnoSender email address. Required before the campaign can start.
campaign[from_name]bodystringnoSender display name.
campaign[end_action_type]bodystringnoWhat happens after a victim acts. One of nothing, redirect_to_course, message_page, redirect_to_url. Defaults to message_page.
campaign[end_action_url]bodystringnoExternal URL to redirect to. Required (and must be http/https, not loop back to a platform domain) when end_action_type is redirect_to_url.
campaign[end_action_html]bodystringnoCustom HTML message page. Required when end_action_type is message_page (auto-seeded with a default if omitted).
campaign[group_ids]bodyarray of integernoIds of contact groups to target.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"campaign": {
"name": "Q2 Invoice Lure",
"delivery_mode": "immediate",
"email_subject": "Your April invoice is ready",
"email_content": "<p>Hello {{first_name}}…</p>",
"from_email": "billing@officelogin.in",
"from_name": "Accounts Payable",
"platform_domain_id": 5,
"end_action_type": "redirect_to_course",
"course_id": 7,
"group_ids": [3]
}
}' \
https://platform.phishspot.com/api/v1/accounts/11/campaigns

Response 201 Created — the newly created campaign object (same shape as GET /campaigns/:id).

Status codes

CodeWhen
201Campaign created.
400The campaign object is missing from the body (ParameterMissing).
403Token’s user is not authorized to create campaigns in the account.
404account_id is not an account the token’s user belongs to.
422Validation failed — e.g. blank/duplicate name, invalid delivery_mode/end_action_type enum, malformed HTML/CSS, disallowed merge tag, or missing end_action_url/end_action_html for the chosen end_action_type. Body: { "errors": { "<field>": ["…"] } }.

Fetches a single campaign by id (shallow route, not nested under account). Use it to read full campaign content and the action flags that tell you which transitions are currently allowed. Auth: Bearer; role: any role.

Parameters

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer). Must belong to an account the token’s user is a member of.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/campaigns/42

Response 200 OK — the campaign object.

FieldTypeDescription
idintegerCampaign id.
account_idintegerOwning account id.
namestringCampaign name.
statestringLifecycle state: draft, in_progress, paused, cancelled, done, or scheduled.
delivery_modestringimmediate, scheduled, or staggered.
delivery_schedulestring | nullRaw delivery-schedule string (only meaningful for scheduled mode).
created_atstringISO8601 timestamp.
updated_atstringISO8601 timestamp.
email_subjectstring | nullEmail subject.
email_contentstring | nullHTML email body.
landing_htmlstring | nullLanding-page HTML.
domainstring | nullName of the associated PlatformDomain (e.g. officelogin.in), or null if none set.
course_idinteger | nullAssociated course id, or null.
groupsarrayTargeted groups, each { "id": integer, "name": string }.
statisticsobjectPresent only when state is in_progress, paused, or done. Object with total_contacts (integer), total_deliverables (integer), completion_percentage (float).
can_startbooleanWhether start/schedule is allowed now (true for draft/scheduled).
can_pausebooleanWhether pause is allowed now (true only when in_progress).
can_cancelbooleanWhether cancel is allowed now (true for in_progress/paused/scheduled).
{
"id": 42,
"account_id": 11,
"name": "Q2 Invoice Lure",
"state": "draft",
"delivery_mode": "immediate",
"delivery_schedule": null,
"created_at": "2026-05-01T09:00:00.000Z",
"updated_at": "2026-05-01T09:00:00.000Z",
"email_subject": "Your April invoice is ready",
"email_content": "<p>Hello {{first_name}}…</p>",
"landing_html": "<form>…</form>",
"domain": "officelogin.in",
"course_id": 7,
"groups": [{ "id": 3, "name": "Finance" }],
"can_start": true,
"can_pause": false,
"can_cancel": false
}

Status codes

CodeWhen
200Campaign returned.
404No campaign with that id in any account the token’s user belongs to (includes cross-account access attempts).

Updates an existing campaign. Editing is only permitted while the campaign is in draft or scheduled state (the authorization policy rejects edits to running/finished campaigns). Auth: Bearer; role: any role.

Parameters

Body params are wrapped in a campaign object; the same permitted keys as POST apply (all optional on update — send only the fields you want to change).

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer).
campaign[…]bodynoAny subset of the keys listed under POST /accounts/:account_id/campaigns (name, delivery_mode, delivery_schedule, email_subject, email_content, landing_html, landing_css, landing_page_enabled, platform_domain_id, course_id, from_email, from_name, end_action_type, end_action_url, end_action_html, group_ids[]).

Request

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "campaign": { "email_subject": "Action required: invoice overdue" } }' \
https://platform.phishspot.com/api/v1/campaigns/42

Response 200 OK — the updated campaign object (same shape as GET /campaigns/:id).

Status codes

CodeWhen
200Campaign updated.
400The campaign object is missing from the body (ParameterMissing).
403Campaign is not in draft/scheduled state (editing locked), or user not authorized.
404Campaign not found in the user’s accounts.
422Validation failed (same validations as POST). Body: { "errors": { … } }.

Permanently deletes a campaign and its dependent records (recipients, deliverables, events, replies). Allowed in any state. Auth: Bearer; role: any role.

No parameters beyond the bearer token and the path id.

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer).

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/campaigns/42

Response 204 No Content — empty body.

Status codes

CodeWhen
204Campaign deleted.
403User not authorized to delete the campaign.
404Campaign not found in the user’s accounts.

Transitions a draft or scheduled campaign to in_progress and enqueues the send jobs. Before sending, the server runs a readiness preflight and rejects the request if the campaign is incomplete. Auth: Bearer; role: any role.

No parameters beyond the bearer token and the path id.

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer).

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/campaigns/42/start

Response 200 OK — the campaign object with state: "in_progress".

Status codes

CodeWhen
200Campaign started; sends enqueued.
403Campaign is not in a startable state (draft/paused/scheduled).
404Campaign not found in the user’s accounts.
422Readiness preflight failed. Body: { "errors": ["…", "…"] } (a flat array of human-readable messages). Triggers include: missing email subject, missing email content, missing sender email, no platform domain set, platform domain not active or sending-blocked, no recipients targeted, and end-action gaps (missing course for redirect_to_course, missing URL for redirect_to_url, missing HTML for message_page).

Marks an in-progress campaign as done (completed). Use it to end a running campaign early; pending sends are not re-enqueued. Auth: Bearer; role: any role.

No parameters beyond the bearer token and the path id.

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer).

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/campaigns/42/stop

Response 200 OK — the campaign object with state: "done".

Status codes

CodeWhen
200Campaign marked done.
403Campaign is not in_progress.
404Campaign not found in the user’s accounts.
422State transition rejected by the model. Body: { "errors": { … } }.

Pauses an in-progress campaign (state → paused), halting further scheduled sends. Resume by calling start again. Auth: Bearer; role: any role.

No parameters beyond the bearer token and the path id.

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer).

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/campaigns/42/pause

Response 200 OK — the campaign object with state: "paused".

Status codes

CodeWhen
200Campaign paused.
403Campaign is not in_progress.
404Campaign not found in the user’s accounts.
422State transition rejected by the model. Body: { "errors": { … } }.

Cancels a campaign (state → cancelled). Allowed from in_progress, paused, or scheduled (not from draft or already-finished campaigns). Auth: Bearer; role: any role.

No parameters beyond the bearer token and the path id.

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer).

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/campaigns/42/cancel

Response 200 OK — the campaign object with state: "cancelled".

Status codes

CodeWhen
200Campaign cancelled.
403Campaign is not in a cancellable state (in_progress/paused/scheduled).
404Campaign not found in the user’s accounts.
422State transition rejected by the model. Body: { "errors": { … } }.

Clones the campaign into a fresh draft (with a numbered-suffix name like "Q2 Invoice Lure (1)"), copying content, targeted groups, and recipients but resetting state, schedule, and snapshot. Use it to re-run or branch a campaign. Auth: Bearer; role: any role.

No parameters beyond the bearer token and the path id.

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer) of the source campaign.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/campaigns/42/duplicate

Response 201 Created — the new draft campaign object (same shape as GET /campaigns/:id), with a new id and state: "draft".

Status codes

CodeWhen
201Duplicate created.
403User not authorized.
404Source campaign not found in the user’s accounts.
422The duplicate failed to save (validation). Body: { "errors": { … } }.

Schedules a draft campaign for a future send (state → scheduled). Runs the same readiness preflight as start, plus time-validity checks. Auth: Bearer; role: any role.

Parameters

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer).
scheduled_atbodystringyesTarget send time as a local (account-timezone) datetime, no offset — e.g. 2026-06-10T09:00 (the value a datetime-local input produces). It is interpreted in the account’s timezone (falling back to UTC if the account has none) and converted to UTC server-side. Must be in the future and at least 5 minutes from now. Not wrapped in a campaign object — sent as a top-level body key.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "scheduled_at": "2026-06-10T09:00" }' \
https://platform.phishspot.com/api/v1/campaigns/42/schedule

Response 200 OK — the campaign object with state: "scheduled".

Status codes

CodeWhen
200Campaign scheduled.
403Campaign is not in a startable state (must be draft).
404Campaign not found in the user’s accounts.
422Scheduling failed. Body: { "errors": ["…"] } (flat array). Triggers: scheduled_at blank, unparseable datetime, time in the past, time less than 5 minutes from now, or the start-readiness preflight failing (same content/sender/domain/recipient/end-action checks as start).

Cancels a pending schedule, returning the campaign to draft and removing its queued send job. Only valid for a scheduled campaign. Auth: Bearer; role: any role.

No parameters beyond the bearer token and the path id.

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer).

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/campaigns/42/cancel_schedule

Response 200 OK — the campaign object (state returned to draft).

Status codes

CodeWhen
200Schedule cancelled.
403User not authorized (policy requires scheduled state).
404Campaign not found in the user’s accounts.
422Campaign is not currently scheduled. Body: { "error": "Campaign is not scheduled (state: <state>); nothing to cancel." } (note the singular error key here).

Returns aggregated campaign statistics: the overall engagement funnel plus per-group and per-department breakdowns. Use it to render dashboards and reports. Auth: Bearer; role: any role.

Parameters

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/campaigns/42/results

Response 200 OK — statistics object.

FieldTypeDescription
campaign_idintegerCampaign id.
namestringCampaign name.
funnelobjectOverall engagement funnel (counts + rates). See sub-fields below.
groupsarrayPer-group breakdown. Empty array if the campaign targets no groups. See sub-fields below.
departmentsarrayPer-department breakdown (contacts grouped by their department). Empty if no departments. Same numeric sub-fields as a group entry, with name but no id.

funnel sub-fields:

FieldTypeDescription
sentintegerDistinct contacts the email was successfully sent to.
openedintegerDistinct contacts who opened.
clickedintegerDistinct contacts who clicked.
submittedintegerDistinct contacts who submitted data on the landing page.
trainedintegerDeliverables that reached the educated (training-completed) state.
repliedintegerDistinct contacts who replied to the phishing email (side-channel signal, not part of the click/submit funnel).
open_ratefloatopened / sent as a percentage (1 decimal).
click_ratefloatclicked / sent percentage.
submit_ratefloatsubmitted / sent percentage.
train_ratefloattrained / sent percentage.
reply_ratefloatreplied / sent percentage.

Each groups[] entry: name (string), id (integer), total_contacts (integer), sent, opened, clicked, submitted, trained (integers), and open_rate, click_rate, submit_rate, train_rate (floats).

{
"campaign_id": 42,
"name": "Q2 Invoice Lure",
"funnel": {
"sent": 120,
"opened": 84,
"clicked": 37,
"submitted": 12,
"trained": 9,
"replied": 3,
"open_rate": 70.0,
"click_rate": 30.8,
"submit_rate": 10.0,
"train_rate": 7.5,
"reply_rate": 2.5
},
"groups": [
{
"name": "Finance",
"id": 3,
"total_contacts": 60,
"sent": 60,
"opened": 45,
"clicked": 22,
"submitted": 8,
"trained": 6,
"open_rate": 75.0,
"click_rate": 36.7,
"submit_rate": 13.3,
"train_rate": 10.0
}
],
"departments": [
{
"name": "Accounting",
"total_contacts": 40,
"sent": 40,
"opened": 30,
"clicked": 15,
"submitted": 5,
"trained": 4,
"open_rate": 75.0,
"click_rate": 37.5,
"submit_rate": 12.5,
"train_rate": 10.0
}
]
}

Status codes

CodeWhen
200Statistics returned.
404Campaign not found in the user’s accounts.

Returns a paginated, filterable list of campaign recipients with their per-contact delivery stage, training status, and reply flag. Recipients are ordered by contact last name then first name. Auth: Bearer; role: any role.

Parameters

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer).
pagequeryintegerno1-based page number; values below 1 are clamped to 1. Defaults to 1. Page size is fixed at 25.
stagequerystringnoFilter by funnel stage: sent, opened, clicked, submitted, or trained. all (or omitted) returns everyone. Filters are cumulative-by-stage (e.g. opened includes those who later clicked/submitted/were educated).
repliedquerybooleannoWhen truthy (true/1), restrict to contacts who replied to the email.
group_idqueryintegernoRestrict to contacts in this group (must belong to the campaign’s account).
departmentquerystringnoRestrict to contacts whose department matches this exact value.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://platform.phishspot.com/api/v1/campaigns/42/recipients?stage=clicked&page=1"

Response 200 OK — paginated recipients.

FieldTypeDescription
campaign_idintegerCampaign id.
pageintegerCurrent page number.
per_pageintegerPage size (always 25).
totalintegerTotal recipients matching the filters (across all pages).
recipientsarrayRecipient rows (see sub-fields).

Each recipients[] entry:

FieldTypeDescription
idintegerContact id.
contact_idintegerContact id (same value as id).
emailstringContact email.
full_namestringContact full name.
statusstringDelivery state: pending, sent, delivered, bounced, opened, clicked, submitted, or educated.
stagestringSame value as status (alias).
training_statusstringnot_started, in_progress, or completed.
repliedbooleanWhether the contact replied to the phishing email.
{
"campaign_id": 42,
"page": 1,
"per_page": 25,
"total": 37,
"recipients": [
{
"id": 901,
"contact_id": 901,
"email": "jane.doe@victimco.com",
"full_name": "Jane Doe",
"status": "clicked",
"stage": "clicked",
"training_status": "in_progress",
"replied": false
}
]
}

Status codes

CodeWhen
200Recipients returned.
404Campaign not found in the user’s accounts, or a group_id/department lookup references a record outside the campaign’s account.

Returns a paginated list of inbound replies recipients sent back to the phishing email, newest first. Use it to surface engaged targets and review reply content. Auth: Bearer; role: any role.

Parameters

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer).
pagequeryintegerno1-based page number; clamped to a minimum of 1. Defaults to 1. Page size is fixed at 25.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://platform.phishspot.com/api/v1/campaigns/42/replies?page=1"

Response 200 OK — paginated replies.

FieldTypeDescription
campaign_idintegerCampaign id.
pageintegerCurrent page number.
per_pageintegerPage size (always 25).
totalintegerTotal replies for the campaign.
repliesarrayReply rows (see sub-fields).

Each replies[] entry:

FieldTypeDescription
idintegerReply id.
from_emailstringSender (recipient) email address.
received_atstringISO8601 timestamp of when the reply was received.
subjectstringReply subject line.
excerptstringPlain-text excerpt of the reply body (truncated).
attachments_countintegerNumber of attachments on the reply.
{
"campaign_id": 42,
"page": 1,
"per_page": 25,
"total": 3,
"replies": [
{
"id": 5501,
"from_email": "jane.doe@victimco.com",
"received_at": "2026-05-02T11:24:00Z",
"subject": "Re: Your April invoice is ready",
"excerpt": "Is this really from accounting? I don't recognize…",
"attachments_count": 0
}
]
}

Status codes

CodeWhen
200Replies returned.
404Campaign not found in the user’s accounts.

Returns the chronological event timeline for a single contact within the campaign (sent → opened → clicked → submitted → training, etc.). Use it to inspect one victim’s full interaction history. Auth: Bearer; role: any role.

Parameters

NameInTypeRequiredDescription
idpathstringyesCampaign id (camp_… or integer).
contact_idqueryintegeryesId of the contact whose timeline to return. Must belong to the campaign’s account.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://platform.phishspot.com/api/v1/campaigns/42/timeline?contact_id=901"

Response 200 OK — the contact’s event list, ordered oldest-first.

FieldTypeDescription
campaign_idintegerCampaign id.
contact_idintegerContact id.
eventsarrayEvents for this contact (see sub-fields).

Each events[] entry:

FieldTypeDescription
genrestringEvent type: sent, delivered, bounced, opened, clicked, submitted_data, started_training, completed_training, failed_quiz, passed_quiz, or replied.
created_atstringISO8601 timestamp of the event.
metadataobject | nullArbitrary event metadata (e.g. user agent, IP, quiz details), as stored.
{
"campaign_id": 42,
"contact_id": 901,
"events": [
{ "genre": "sent", "created_at": "2026-05-01T09:05:00Z", "metadata": {} },
{ "genre": "opened", "created_at": "2026-05-01T09:41:00Z", "metadata": { "ua": "Mozilla/5.0" } },
{ "genre": "clicked", "created_at": "2026-05-01T09:42:00Z", "metadata": { "ip": "203.0.113.7" } }
]
}

Status codes

CodeWhen
200Timeline returned.
404Campaign not found in the user’s accounts, or contact_id does not reference a contact in the campaign’s account (a missing/invalid contact_id raises a not-found).

The phishing-template library is the catalog of ready-made phishing scenarios (email + landing page + post-click action) that an account can deploy into a real campaign. Templates come in two flavors: curated (platform-provided, shared with every account, read-only) and custom (created by an account, visible only to that account). Templates are organized into a tree of categories (up to three levels deep). Deploying a template creates a new draft campaign pre-filled with the template’s content — it never sends any email.

GET /accounts/:account_id/phishing_templates

Section titled “GET /accounts/:account_id/phishing_templates”

Lists templates visible to an account, paginated 12 per page. Use the tab parameter to switch between the shared curated library and the account’s own custom templates, and category/search to narrow the results. Returns metadata only (no HTML blobs) — call the show endpoint for full content. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer). Must be an account the token’s user belongs to.
tabquerystringnoWhich library to list. custom returns this account’s own templates; any other value (or omitted) returns the shared curated library. Default: curated.
categoryquerystring or arraynoOne or more category ids (tcat_… prefix ids or integers) to filter by. Matches templates assigned to the given category or any of its descendants. Pass multiple as repeated category[] params. Unrecognized ids are ignored.
searchquerystringnoCase-insensitive substring match against template name and description.
pagequeryintegerno1-based page number. Values below 1 are clamped to 1. Default: 1. Page size is fixed at 12.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://platform.phishspot.com/api/v1/accounts/11/phishing_templates?tab=curated&category=tcat_abc123&search=invoice&page=1"

Response 200 OK — pagination envelope plus a templates array of template metadata.

FieldTypeDescription
tabstringThe resolved tab: "curated" or "custom".
pageintegerCurrent page number.
per_pageintegerPage size (always 12).
totalintegerTotal number of templates matching the filters (across all pages).
templatesarrayArray of template objects (see fields below).
templates[].idintegerRaw numeric template id.
templates[].namestringTemplate name.
templates[].descriptionstring | nullFree-text description.
templates[].curatedbooleantrue for platform-provided templates, false for account-owned.
templates[].draftbooleantrue if the template is an unpublished draft (missing required content). Drafts cannot be deployed.
templates[].email_subjectstring | nullThe phishing email subject line.
templates[].landing_page_enabledbooleanWhether the template includes a hosted landing page.
templates[].created_atstring (ISO 8601)Creation timestamp.
templates[].updated_atstring (ISO 8601)Last-update timestamp.
templates[].template_idstringPrefixed id (tmpl_…). Use this in the show/deploy paths.
templates[].categoriesarrayCategories this template is assigned to.
templates[].categories[].idintegerRaw numeric category id.
templates[].categories[].category_idstringPrefixed category id (tcat_…).
templates[].categories[].namestringLocalized category name (current request locale, falling back to English).
{
"tab": "curated",
"page": 1,
"per_page": 12,
"total": 37,
"templates": [
{
"id": 84,
"name": "Unpaid Invoice Reminder",
"description": "Spoofed accounts-payable invoice with a credential-harvesting login page.",
"curated": true,
"draft": false,
"email_subject": "Action required: invoice #44021 is overdue",
"landing_page_enabled": true,
"created_at": "2026-01-14T09:12:00.000Z",
"updated_at": "2026-03-02T16:40:11.000Z",
"template_id": "tmpl_8x2k9q",
"categories": [
{ "id": 5, "category_id": "tcat_abc123", "name": "Finance" }
]
}
]
}

Status codes

CodeWhen
200Templates listed (the array may be empty).
404account_id is not an account the token’s user belongs to. Body: {"error":"Account not found"}.

GET /accounts/:account_id/phishing_template_categories

Section titled “GET /accounts/:account_id/phishing_template_categories”

Returns the full category tree (roots with nested children, up to three levels) for the template-library filter UI. Useful for building a category picker before calling the templates list with category=. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer). Must be an account the token’s user belongs to.

(Categories are global, not account-scoped — the account_id only gates access. No other parameters.)

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/accounts/11/phishing_template_categories

Response 200 OK — a categories array of root categories, each recursively embedding its children.

FieldTypeDescription
categoriesarrayRoot categories, ordered by position.
categories[].idintegerRaw numeric category id.
categories[].category_idstringPrefixed category id (tcat_…).
categories[].namestringLocalized category name (request locale, falling back to English).
categories[].slugstringURL-safe slug (unique, derived from the English name).
categories[].depthintegerTree depth: 0 for roots, 1 for children, 2 for grandchildren.
categories[].is_leafbooleantrue when the category has no children.
categories[].childrenarrayNested child categories with the same shape (empty array for leaves).
{
"categories": [
{
"id": 1,
"category_id": "tcat_root01",
"name": "Finance",
"slug": "finance",
"depth": 0,
"is_leaf": false,
"children": [
{
"id": 5,
"category_id": "tcat_abc123",
"name": "Invoices",
"slug": "invoices",
"depth": 1,
"is_leaf": true,
"children": []
}
]
}
]
}

Status codes

CodeWhen
200Category tree returned (may be an empty array).
404account_id is not an account the token’s user belongs to. Body: {"error":"Account not found"}.

Returns a single template with its full content — email body, landing-page HTML/CSS, and post-click (end action) configuration. Use this to render a preview or to inspect what a deploy will copy into a campaign. This route is shallow (no account_id in the path); the token’s user must be able to see the template (their own custom templates plus all curated ones). Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesTemplate id (tmpl_… or integer).

No parameters beyond the bearer token.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/phishing_templates/tmpl_8x2k9q

Response 200 OK — the full template object.

FieldTypeDescription
idintegerRaw numeric template id.
namestringTemplate name.
descriptionstring | nullFree-text description.
curatedbooleantrue for platform-provided templates, false for account-owned.
draftbooleantrue if unpublished. Drafts cannot be deployed.
email_subjectstring | nullPhishing email subject line (may contain merge tags).
email_contentstring | nullFull phishing email HTML body.
landing_htmlstring | nullLanding-page HTML.
landing_cssstring | nullLanding-page CSS.
landing_page_enabledbooleanWhether a hosted landing page is included.
end_action_typestringWhat happens after a victim submits the landing page. One of nothing, redirect_to_course, message_page, redirect_to_url.
end_action_urlstring | nullTarget URL when end_action_type is redirect_to_url (must be http/https).
end_action_htmlstring | nullHTML shown when end_action_type is message_page (e.g. the awareness page).
created_atstring (ISO 8601)Creation timestamp.
updated_atstring (ISO 8601)Last-update timestamp.
template_idstringPrefixed id (tmpl_…).
course_idinteger | nullLinked e-learning course id (used when end_action_type is redirect_to_course).
publishablebooleantrue when all required fields (name, subject, email body, landing HTML) are present.
categoriesarrayAssigned categories: each with id (integer), category_id (tcat_…), and name.
{
"id": 84,
"name": "Unpaid Invoice Reminder",
"description": "Spoofed accounts-payable invoice with a credential-harvesting login page.",
"curated": true,
"draft": false,
"email_subject": "Action required: invoice #44021 is overdue",
"email_content": "<html><body><p>Dear {{first_name}}, your invoice is overdue…</p></body></html>",
"landing_html": "<form action=\"#\">…</form>",
"landing_css": "body { font-family: sans-serif; }",
"landing_page_enabled": true,
"end_action_type": "message_page",
"end_action_url": null,
"end_action_html": "<h1>You've been phished by a simulation.</h1>",
"created_at": "2026-01-14T09:12:00.000Z",
"updated_at": "2026-03-02T16:40:11.000Z",
"template_id": "tmpl_8x2k9q",
"course_id": null,
"publishable": true,
"categories": [
{ "id": 5, "category_id": "tcat_abc123", "name": "Finance" }
]
}

Status codes

CodeWhen
200Template returned.
403The template is not visible to the token’s user (another account’s custom template). Body: {"error":"You are not authorized to perform this action"}.
404No template matches id. Body: {"error":"Resource not found"}.

Creates a new draft campaign in the target account, pre-filled with the template’s content (email subject/body, landing HTML/CSS, end action, course). This route is shallow, so the deploying account is passed in the body as account_id. With quick_launch=true it additionally adds all of the account’s contacts as recipients and advances the campaign to the review step. This endpoint never sends any email — a human launches the campaign from the PhishSpot UI. Drafts cannot be deployed. Auth: Bearer; role: any member of the target account.

Parameters

NameInTypeRequiredDescription
idpathstringyesTemplate id to deploy (tmpl_… or integer). Must not be a draft template.
account_idbodystringyesAccount to create the campaign in (acct_… or integer). Must be an account the token’s user belongs to.
quick_launchbodybooleannoWhen truthy (true, "1", etc.), bulk-adds every account contact as a recipient and marks wizard steps 1–5 complete so the campaign lands on the review step. Requires an active sending domain and at least one contact. Default: false.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "account_id": "acct_4f8a2c", "quick_launch": true }' \
https://platform.phishspot.com/api/v1/phishing_templates/tmpl_8x2k9q/deploy

Response 201 Created — the newly created campaign (same shape as the campaign show endpoint). A freshly deployed campaign is in the draft state, so the statistics block is omitted (it only appears once a campaign is in progress, paused, or done).

FieldTypeDescription
idintegerRaw numeric campaign id.
account_idintegerOwning account id.
namestringAuto-generated name: "<Template name> - YYYY-MM-DD HH:MM:SS" (with a numeric suffix on collision).
statestringLifecycle state — draft immediately after deploy. One of draft, in_progress, paused, cancelled, done, scheduled.
delivery_modestring | nullimmediate, scheduled, or staggered (not set by deploy).
delivery_scheduleobject | nullDelivery schedule config (not set by deploy).
created_atstring (ISO 8601)Creation timestamp.
updated_atstring (ISO 8601)Last-update timestamp.
email_subjectstring | nullCopied from the template.
email_contentstring | nullCopied from the template.
landing_htmlstring | nullCopied from the template.
domainstring | nullSending/landing platform domain name (auto-selected from the account’s available domains, may be null).
course_idinteger | nullLinked course id (template’s course, or the account default).
groupsarrayContact groups on the campaign — empty right after deploy. Each: id, name.
can_startbooleanWhether the campaign can transition to start.
can_pausebooleanWhether the campaign can be paused.
can_cancelbooleanWhether the campaign can be cancelled.
{
"id": 512,
"account_id": 11,
"name": "Unpaid Invoice Reminder - 2026-06-02 14:30:07",
"state": "draft",
"delivery_mode": null,
"delivery_schedule": null,
"created_at": "2026-06-02T14:30:07.000Z",
"updated_at": "2026-06-02T14:30:07.000Z",
"email_subject": "Action required: invoice #44021 is overdue",
"email_content": "<html><body><p>Dear {{first_name}}…</p></body></html>",
"landing_html": "<form action=\"#\">…</form>",
"domain": "officelogin.in",
"course_id": null,
"groups": [],
"can_start": false,
"can_pause": false,
"can_cancel": false
}

Status codes

CodeWhen
201Campaign created from the template.
403The template is a draft (drafts cannot be deployed), or it is not visible to the token’s user. Body: {"error":"You are not authorized to perform this action"}.
404No template matches id, or the body account_id is not an account the token’s user belongs to. Body: {"error":"Resource not found"} (template) / {"error":"Account not found"} (account).
422quick_launch=true but the account has no active sending domain ("Quick launch needs an active sending domain for this account.") or no contacts ("Quick launch needs at least one contact in the account."). Body: {"error":"<message>"}.

Contacts are the employees you target with phishing simulations; groups are named collections used to scope campaigns. Both are account-scoped: collection actions are nested under /accounts/:account_id/…, while reads/writes on an individual record use the shallow /contacts/:id and /groups/:id routes. Membership in the account is required for every endpoint — all policy checks pass for any account member (read and write alike), so there is no admin/editor distinction here. The only write gate is that a group participating in an active campaign (in_progress or paused) is locked and cannot be updated, deleted, or have its membership changed.

Lists every contact in the account, ordered by last name then first name, with each contact’s groups inlined. Use it to page through or sync your roster. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/accounts/11/contacts

Response 200 OK — a JSON array of contact objects (see the contact fields below).

FieldTypeDescription
idintegerContact primary key.
account_idintegerOwning account id.
first_namestringGiven name.
last_namestring|nullSurname.
emailstringEmail address (unique per account).
telephonestring|nullPhone number.
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
full_namestringConvenience: "first_name last_name" trimmed.
groupsarrayGroups this contact belongs to; each { id, name }.
groups[].idintegerGroup id.
groups[].namestringGroup name (normalized, snake_case for manual groups).
[
{
"id": 501,
"account_id": 11,
"first_name": "Ada",
"last_name": "Kowalska",
"email": "ada.kowalska@example.com",
"telephone": "+48 600 123 456",
"created_at": "2026-05-01T09:30:00.000Z",
"updated_at": "2026-05-12T14:02:11.000Z",
"full_name": "Ada Kowalska",
"groups": [
{ "id": 90, "name": "finance" }
]
}
]

Status codes

CodeWhen
200Contacts returned (possibly an empty array).
404account_id is not an account the token’s user belongs to.

Creates a single contact in the account. Use the import endpoint instead for bulk loads. Auth: Bearer; role: read (any role).

Parameters

Body params are wrapped in a contact object.

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
first_namebodystringyesGiven name (max 255). Required by the model.
emailbodystringyesEmail address. Must match a standard email format and be unique within the account (case-insensitive). Max 255.
last_namebodystringnoSurname (max 255).
telephonebodystringnoPhone number (max 50). Must match +CC (NNN) NNN-NNNN-style formats; blank allowed.
group_idsbodyarraynoArray of group ids to attach the contact to on creation.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "contact": { "first_name": "Ada", "last_name": "Kowalska", "email": "ada.kowalska@example.com", "telephone": "+48 600 123 456", "group_ids": [90] } }' \
https://platform.phishspot.com/api/v1/accounts/11/contacts

Response 201 Created — the created contact, using the same fields as the list endpoint above.

{
"id": 501,
"account_id": 11,
"first_name": "Ada",
"last_name": "Kowalska",
"email": "ada.kowalska@example.com",
"telephone": "+48 600 123 456",
"created_at": "2026-05-01T09:30:00.000Z",
"updated_at": "2026-05-01T09:30:00.000Z",
"full_name": "Ada Kowalska",
"groups": [
{ "id": 90, "name": "finance" }
]
}

Status codes

CodeWhen
201Contact created.
400Body is missing the top-level contact key.
404account_id is not an account the token’s user belongs to.
422Validation failed (e.g. missing first_name, missing/malformed email, or duplicate email within the account). Body is { "errors": { … } }.

POST /accounts/:account_id/contacts/import

Section titled “POST /accounts/:account_id/contacts/import”

Bulk-imports contacts into the account from CSV. Existing contacts (matched by email) are updated with non-blank values; new ones are created; groups named in the data are created and associations are made. Provide either raw CSV text in csv or a JSON array in contacts — the array is converted to CSV server-side using the canonical header order. Auth: Bearer; role: read (any role).

The canonical CSV header order is: first_name, last_name, email, telephone, groups, department, title, location. In the groups column, multiple groups are separated by | (a pipe). When using the JSON contacts form, each row’s groups may be an array (e.g. ["finance","exec"]) which is joined with | automatically.

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
csvbodystringconditionalRaw CSV text with the canonical header row. Required if contacts is omitted. Takes precedence if both are given.
contactsbodyarrayconditionalArray of row objects keyed by the canonical headers. Required if csv is omitted. Each row’s groups may be a string or an array of group names.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "contacts": [
{ "first_name": "Ada", "last_name": "Kowalska", "email": "ada.kowalska@example.com", "telephone": "+48600123456", "groups": ["finance"], "department": "Finance", "title": "Analyst", "location": "Warsaw" },
{ "first_name": "Jan", "email": "jan.nowak@example.com", "groups": ["finance", "exec"] }
] }' \
https://platform.phishspot.com/api/v1/accounts/11/contacts/import

Response 200 OK — a summary of how the rows were processed.

FieldTypeDescription
createdintegerNumber of new contacts inserted.
updatedintegerNumber of existing contacts (matched by email) updated with new non-blank values.
failedintegerNumber of rows rejected as invalid. (A downloadable failed-rows CSV report is attached to the account.)
{
"created": 1,
"updated": 1,
"failed": 0
}

Status codes

CodeWhen
200Import ran; returns the {created, updated, failed} summary.
404account_id is not an account the token’s user belongs to.
422Neither csv nor contacts was provided. Body is { "error": "Provide either csv or contacts." }.

Fetches a single contact by id, scoped to the token user’s accounts. Use it to read one contact without listing the whole account. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesContact id (cont_… or integer).

No parameters beyond the bearer token and path id.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/contacts/cont_abc123

Response 200 OK — the contact, using the same fields as the list endpoint.

{
"id": 501,
"account_id": 11,
"first_name": "Ada",
"last_name": "Kowalska",
"email": "ada.kowalska@example.com",
"telephone": "+48 600 123 456",
"created_at": "2026-05-01T09:30:00.000Z",
"updated_at": "2026-05-12T14:02:11.000Z",
"full_name": "Ada Kowalska",
"groups": [
{ "id": 90, "name": "finance" }
]
}

Status codes

CodeWhen
200Contact found.
404No contact with that id in any account the token’s user belongs to.

Updates a single contact. Send only the fields you want to change, wrapped in a contact object. Auth: Bearer; role: read (any role).

Parameters

Body params are wrapped in a contact object.

NameInTypeRequiredDescription
idpathstringyesContact id (cont_… or integer).
first_namebodystringnoGiven name (max 255). Cannot be cleared to blank — it is required.
last_namebodystringnoSurname (max 255).
emailbodystringnoEmail address. Must stay valid and unique per account (case-insensitive). Max 255.
telephonebodystringnoPhone number (max 50, format-validated; blank allowed).
group_idsbodyarraynoReplaces the contact’s group membership with this exact set of group ids.

Request

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "contact": { "title": "Senior Analyst", "group_ids": [90, 91] } }' \
https://platform.phishspot.com/api/v1/contacts/cont_abc123

Response 200 OK — the updated contact, using the same fields as the list endpoint.

{
"id": 501,
"account_id": 11,
"first_name": "Ada",
"last_name": "Kowalska",
"email": "ada.kowalska@example.com",
"telephone": "+48 600 123 456",
"created_at": "2026-05-01T09:30:00.000Z",
"updated_at": "2026-05-20T08:15:00.000Z",
"full_name": "Ada Kowalska",
"groups": [
{ "id": 90, "name": "finance" },
{ "id": 91, "name": "exec" }
]
}

Status codes

CodeWhen
200Contact updated.
400Body is missing the top-level contact key.
404No contact with that id in any account the token’s user belongs to.
422Validation failed (blank first_name, invalid/duplicate email, bad telephone format). Body is { "errors": { … } }.

Permanently deletes a contact and its group memberships, deliverables, events, and results. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesContact id (cont_… or integer).

No parameters beyond the bearer token and path id.

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/contacts/cont_abc123

Response 204 No Content — empty body.

Status codes

CodeWhen
204Contact deleted.
404No contact with that id in any account the token’s user belongs to.

Lists every group in the account, ordered by name, with each group’s contacts inlined. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).

Response 200 OK — a JSON array of group objects (see the group fields below).

FieldTypeDescription
idintegerGroup primary key.
account_idintegerOwning account id.
namestringGroup name. For manual groups this is normalized to snake_case (spaces → underscores, non-alphanumerics stripped, lowercased).
contact_countintegerCached count of contacts in the group.
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
contactsarrayMembers of the group.
contacts[].idintegerContact id.
contacts[].emailstringContact email.
contacts[].first_namestringContact given name.
contacts[].last_namestring|nullContact surname.
contacts[].full_namestring"first_name last_name" trimmed.
[
{
"id": 90,
"account_id": 11,
"name": "finance",
"contact_count": 2,
"created_at": "2026-04-10T11:00:00.000Z",
"updated_at": "2026-05-20T08:15:00.000Z",
"contacts": [
{ "id": 501, "email": "ada.kowalska@example.com", "first_name": "Ada", "last_name": "Kowalska", "full_name": "Ada Kowalska" }
]
}
]

Status codes

CodeWhen
200Groups returned (possibly an empty array).
404account_id is not an account the token’s user belongs to.

Creates a group in the account. Manual group names are normalized to snake_case server-side. Auth: Bearer; role: read (any role).

Parameters

Body params are wrapped in a group object.

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
namebodystringyesGroup name (max 255). Normalized to snake_case; must be unique within the account (case-insensitive, compared after normalization).
descriptionbodystringnoFree-text description. Permitted by the controller (the model has no description column, so it is accepted but not persisted/returned).
contact_idsbodyarraynoArray of contact ids to add as members on creation.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "group": { "name": "Finance Team", "contact_ids": [501, 502] } }' \
https://platform.phishspot.com/api/v1/accounts/11/groups

Response 201 Created — the created group, using the same fields as the list endpoint above. Note "Finance Team" is stored and returned as "finance_team".

{
"id": 90,
"account_id": 11,
"name": "finance_team",
"contact_count": 2,
"created_at": "2026-04-10T11:00:00.000Z",
"updated_at": "2026-04-10T11:00:00.000Z",
"contacts": [
{ "id": 501, "email": "ada.kowalska@example.com", "first_name": "Ada", "last_name": "Kowalska", "full_name": "Ada Kowalska" },
{ "id": 502, "email": "jan.nowak@example.com", "first_name": "Jan", "last_name": "Nowak", "full_name": "Jan Nowak" }
]
}

Status codes

CodeWhen
201Group created.
400Body is missing the top-level group key.
404account_id is not an account the token’s user belongs to.
422Validation failed (blank name, or a name that normalizes to a duplicate within the account). Body is { "errors": { … } }.

Fetches a single group by id, scoped to the token user’s accounts. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesGroup id (grp_… or integer).

No parameters beyond the bearer token and path id.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/groups/grp_xyz789

Response 200 OK — the group, using the same fields as the list endpoint.

{
"id": 90,
"account_id": 11,
"name": "finance",
"contact_count": 1,
"created_at": "2026-04-10T11:00:00.000Z",
"updated_at": "2026-05-20T08:15:00.000Z",
"contacts": [
{ "id": 501, "email": "ada.kowalska@example.com", "first_name": "Ada", "last_name": "Kowalska", "full_name": "Ada Kowalska" }
]
}

Status codes

CodeWhen
200Group found.
404No group with that id in any account the token’s user belongs to.

Updates a group’s name (and, optionally, replaces its membership). Blocked if the group is locked by an active campaign. Auth: Bearer; role: read (any role).

Parameters

Body params are wrapped in a group object.

NameInTypeRequiredDescription
idpathstringyesGroup id (grp_… or integer).
namebodystringnoNew group name (max 255). Normalized to snake_case; must remain unique within the account.
descriptionbodystringnoAccepted but not persisted (no model column).
contact_idsbodyarraynoReplaces the group’s membership with this exact set of contact ids.

Request

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "group": { "name": "Finance and Ops", "contact_ids": [501, 503] } }' \
https://platform.phishspot.com/api/v1/groups/grp_xyz789

Response 200 OK — the updated group, using the same fields as the list endpoint.

{
"id": 90,
"account_id": 11,
"name": "finance_and_ops",
"contact_count": 2,
"created_at": "2026-04-10T11:00:00.000Z",
"updated_at": "2026-05-21T10:00:00.000Z",
"contacts": [
{ "id": 501, "email": "ada.kowalska@example.com", "first_name": "Ada", "last_name": "Kowalska", "full_name": "Ada Kowalska" },
{ "id": 503, "email": "ola.wisniewska@example.com", "first_name": "Ola", "last_name": "Wisniewska", "full_name": "Ola Wisniewska" }
]
}

Status codes

CodeWhen
200Group updated.
400Body is missing the top-level group key.
403The group is locked (used in an in_progress/paused campaign) so it cannot be modified.
404No group with that id in any account the token’s user belongs to.
422Validation failed (blank name or a name that normalizes to a duplicate). Body is { "errors": { … } }.

Permanently deletes a group and its contact-group / campaign-group associations (the contacts themselves are not deleted). Blocked if the group is locked by an active campaign. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesGroup id (grp_… or integer).

No parameters beyond the bearer token and path id.

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/groups/grp_xyz789

Response 204 No Content — empty body.

Status codes

CodeWhen
204Group deleted.
403The group is locked (used in an in_progress/paused campaign) so it cannot be deleted.
404No group with that id in any account the token’s user belongs to.

Adds one or more contacts to a group. Contact ids are resolved against the group’s own account — ids that belong to another account, or that don’t exist, are silently dropped. Contacts already in the group are skipped. Blocked if the group is locked. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesGroup id (grp_… or integer).
contact_idsbodyarrayyesContact ids (cont_… or integers) to add. Ids outside the group’s account or non-existent are ignored.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "contact_ids": [501, "cont_def456"] }' \
https://platform.phishspot.com/api/v1/groups/grp_xyz789/add_contacts

Response 200 OK — the updated group (after reload), using the same fields as the list endpoint. The response does not include a separate count of how many were added; compare contact_count / contacts before and after.

{
"id": 90,
"account_id": 11,
"name": "finance",
"contact_count": 2,
"created_at": "2026-04-10T11:00:00.000Z",
"updated_at": "2026-05-22T09:00:00.000Z",
"contacts": [
{ "id": 501, "email": "ada.kowalska@example.com", "first_name": "Ada", "last_name": "Kowalska", "full_name": "Ada Kowalska" },
{ "id": 540, "email": "marek.zielinski@example.com", "first_name": "Marek", "last_name": "Zielinski", "full_name": "Marek Zielinski" }
]
}

Status codes

CodeWhen
200Returns the updated group (even if every supplied id was dropped/already present — it just won’t change).
403The group is locked (used in an in_progress/paused campaign).
404No group with that id in any account the token’s user belongs to.

Removes one or more contacts from a group. Contact ids are resolved against the group’s account; ids not in the group (or outside the account) are ignored. Blocked if the group is locked. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesGroup id (grp_… or integer).
contact_idsbodyarrayyesContact ids (cont_… or integers) to remove from the group.

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "contact_ids": [540] }' \
https://platform.phishspot.com/api/v1/groups/grp_xyz789/remove_contacts

Response 200 OK — the updated group (after reload), using the same fields as the list endpoint.

{
"id": 90,
"account_id": 11,
"name": "finance",
"contact_count": 1,
"created_at": "2026-04-10T11:00:00.000Z",
"updated_at": "2026-05-22T09:10:00.000Z",
"contacts": [
{ "id": 501, "email": "ada.kowalska@example.com", "first_name": "Ada", "last_name": "Kowalska", "full_name": "Ada Kowalska" }
]
}

Status codes

CodeWhen
200Returns the updated group (ids not in the group are simply ignored).
403The group is locked (used in an in_progress/paused campaign).
404No group with that id in any account the token’s user belongs to.

These three resources record the per-recipient telemetry of a campaign. A deliverable is the join between a campaign and a contact (one row per recipient) and tracks its position in the engagement funnel via state. An event is an immutable-ish timeline entry (sent, opened, clicked, …) keyed by genre. A result stores a contact’s answer/score for a single e-learning block.

All endpoints in this section authorize with the resource’s Pundit policy, which permits any team member (any role) to read, create, update, and destroy. There is no admin/editor gate. Listing and creation are nested under an account (/accounts/:account_id/…); show/update/destroy are shallow (/deliverables/:id etc.) and are scoped to accounts the token’s user belongs to — requesting a record outside those accounts returns 404, never another tenant’s data.

GET /api/v1/accounts/:account_id/deliverables

Section titled “GET /api/v1/accounts/:account_id/deliverables”

Lists every deliverable for the account, newest first, optionally filtered to one campaign. Use it to pull the recipient roster and funnel state of a campaign. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
campaign_idquerystringnoRestrict to one campaign (camp_… or integer). When omitted, all deliverables for the account are returned.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://platform.phishspot.com/api/v1/accounts/11/deliverables?campaign_id=42"

Response 200 OK — JSON array of deliverable objects.

FieldTypeDescription
idintegerDeliverable id.
campaign_idintegerOwning campaign.
contact_idintegerTargeted contact.
statestringFunnel state (see enum below).
user_agentstring | nullUser-agent captured on open/click, if any.
ip_addressstring | nullIP captured on open/click, if any.
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
campaignobject | nullPresent when the campaign loads: { id, name, account_id }.
contactobject | nullPresent when the contact loads: { id, email, first_name, last_name, full_name }.
eventsarrayPresent only when the contact has events in this campaign; each item is { id, genre, created_at }.

state is one of: pending (not yet sent), sent, delivered, bounced, opened, clicked, submitted (data entered on landing page), educated (completed training).

[
{
"id": 5012,
"campaign_id": 42,
"contact_id": 880,
"state": "clicked",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"ip_address": "203.0.113.7",
"created_at": "2026-05-30T09:12:44.000Z",
"updated_at": "2026-05-30T10:01:08.000Z",
"campaign": { "id": 42, "name": "Q2 Invoice Lure", "account_id": 11 },
"contact": { "id": 880, "email": "jan.kowalski@acme.test", "first_name": "Jan", "last_name": "Kowalski", "full_name": "Jan Kowalski" },
"events": [
{ "id": 9001, "genre": "sent", "created_at": "2026-05-30T09:12:44.000Z" },
{ "id": 9044, "genre": "opened", "created_at": "2026-05-30T09:58:21.000Z" },
{ "id": 9051, "genre": "clicked", "created_at": "2026-05-30T10:01:08.000Z" }
]
}
]

Status codes

CodeWhen
200Deliverables returned.
403Token user is not authorized to view the account (account.show? denied).
404account_id does not belong to the token user.

POST /api/v1/accounts/:account_id/deliverables

Section titled “POST /api/v1/accounts/:account_id/deliverables”

Creates a deliverable, linking a contact to a campaign. The account_id is derived from the campaign automatically (the supplied account_id path segment selects the account context). Auth: Bearer; role: read (any role).

Parameters

All body params are wrapped in a deliverable object.

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
deliverable.campaign_idbodyintegeryesCampaign to attach to. Validated presence.
deliverable.contact_idbodyintegeryesContact being targeted. Validated presence.
deliverable.statebodystringnoFunnel state; defaults to pending. One of the state enum values. Validated presence.
deliverable.namebodystringnoAccepted by strong params but not persisted (no name column).

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "deliverable": { "campaign_id": 42, "contact_id": 880, "state": "pending" } }' \
https://platform.phishspot.com/api/v1/accounts/11/deliverables

Response 201 Created — the created deliverable (same shape as the show/index object above).

{
"id": 5099,
"campaign_id": 42,
"contact_id": 880,
"state": "pending",
"user_agent": null,
"ip_address": null,
"created_at": "2026-06-02T08:00:00.000Z",
"updated_at": "2026-06-02T08:00:00.000Z",
"campaign": { "id": 42, "name": "Q2 Invoice Lure", "account_id": 11 },
"contact": { "id": 880, "email": "jan.kowalski@acme.test", "first_name": "Jan", "last_name": "Kowalski", "full_name": "Jan Kowalski" }
}

Status codes

CodeWhen
201Deliverable created.
404account_id does not belong to the token user.
422Validation failed (missing campaign_id/contact_id/state, or an invalid state value). Body: { "errors": { … } }.

Fetches a single deliverable, including its campaign, contact, and event timeline. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesDeliverable id (delv_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/deliverables/5012

Response 200 OK — single deliverable object (same fields as the index item above).

{
"id": 5012,
"campaign_id": 42,
"contact_id": 880,
"state": "clicked",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"ip_address": "203.0.113.7",
"created_at": "2026-05-30T09:12:44.000Z",
"updated_at": "2026-05-30T10:01:08.000Z",
"campaign": { "id": 42, "name": "Q2 Invoice Lure", "account_id": 11 },
"contact": { "id": 880, "email": "jan.kowalski@acme.test", "first_name": "Jan", "last_name": "Kowalski", "full_name": "Jan Kowalski" },
"events": [
{ "id": 9001, "genre": "sent", "created_at": "2026-05-30T09:12:44.000Z" }
]
}

Status codes

CodeWhen
200Deliverable returned.
404No deliverable with that id in an account the token user belongs to.

Updates a deliverable — typically to advance its state or re-link campaign/contact. Auth: Bearer; role: read (any role).

Parameters

Body params are wrapped in a deliverable object; send only the fields you want to change.

NameInTypeRequiredDescription
idpathstringyesDeliverable id (delv_… or integer).
deliverable.statebodystringnoNew funnel state (one of the state enum values).
deliverable.campaign_idbodyintegernoReassign campaign (presence still enforced — cannot be blanked).
deliverable.contact_idbodyintegernoReassign contact (presence still enforced).
deliverable.namebodystringnoAccepted but not persisted (no column).

Request

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "deliverable": { "state": "submitted" } }' \
https://platform.phishspot.com/api/v1/deliverables/5012

Response 200 OK — the updated deliverable object (same shape as show).

Status codes

CodeWhen
200Deliverable updated.
404No deliverable with that id in an account the token user belongs to.
422Validation failed (e.g. invalid state, or campaign_id/contact_id blanked). Body: { "errors": { … } }.

Permanently deletes a deliverable (and its dependent campaign_replies). Auth: Bearer; role: read (any role).

No parameters beyond the bearer token and the path id.

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/deliverables/5012

Response 204 No Content — empty body on success.

Status codes

CodeWhen
204Deliverable deleted.
404No deliverable with that id in an account the token user belongs to.

Lists the account’s events newest-first, with optional filtering by campaign, contact, and genre. Use it to reconstruct an engagement timeline. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
campaign_idquerystringnoRestrict to one campaign (camp_… or integer).
contact_idquerystringnoRestrict to one contact (cont_… or integer).
genrequerystringnoRestrict to one genre (see enum below).

genre is one of: sent, delivered, bounced, opened, clicked, submitted_data (data submitted on landing page), started_training, completed_training, failed_quiz, passed_quiz, replied (recipient replied to the phishing email).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://platform.phishspot.com/api/v1/accounts/11/events?campaign_id=42&genre=clicked"

Response 200 OK — JSON array of event objects.

FieldTypeDescription
idintegerEvent id.
account_idintegerOwning account.
campaign_idintegerCampaign the event belongs to.
contact_idintegerContact the event belongs to.
genrestringEvent genre (see enum above).
metadataobjectFree-form JSON (e.g. ip_address, user_agent, submitted fields, quiz data). Defaults to {}.
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
genre_display_namestringHumanized genre (e.g. "Submitted data"); present only when genre is set.
ip_addressstringConvenience copy of metadata.ip_address; present only when set.
user_agentstringConvenience copy of metadata.user_agent; present only when set.
[
{
"id": 9051,
"account_id": 11,
"campaign_id": 42,
"contact_id": 880,
"genre": "clicked",
"metadata": { "ip_address": "203.0.113.7", "user_agent": "Mozilla/5.0" },
"created_at": "2026-05-30T10:01:08.000Z",
"updated_at": "2026-05-30T10:01:08.000Z",
"genre_display_name": "Clicked",
"ip_address": "203.0.113.7",
"user_agent": "Mozilla/5.0"
}
]

Status codes

CodeWhen
200Events returned.
404account_id does not belong to the token user.

Records a new event. The event’s account is set from the path account, and on save the model overrides it to match the campaign’s account. Auth: Bearer; role: read (any role).

Parameters

Body params are wrapped in an event object.

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
event.campaign_idbodyintegeryesCampaign for this event. Validated presence.
event.contact_idbodyintegeryesContact for this event. Validated presence.
event.genrebodystringnoEvent genre; defaults to sent. One of the genre enum values. Validated presence.
event.metadatabodyobjectnoArbitrary JSON hash (permitted as metadata: {}). Defaults to {}.
event.namebodystringnoAccepted by strong params but not persisted (no name column).

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "event": { "campaign_id": 42, "contact_id": 880, "genre": "opened", "metadata": { "ip_address": "203.0.113.7", "user_agent": "Mozilla/5.0" } } }' \
https://platform.phishspot.com/api/v1/accounts/11/events

Response 201 Created — the created event object (same shape as the index item above).

{
"id": 9044,
"account_id": 11,
"campaign_id": 42,
"contact_id": 880,
"genre": "opened",
"metadata": { "ip_address": "203.0.113.7", "user_agent": "Mozilla/5.0" },
"created_at": "2026-05-30T09:58:21.000Z",
"updated_at": "2026-05-30T09:58:21.000Z",
"genre_display_name": "Opened",
"ip_address": "203.0.113.7",
"user_agent": "Mozilla/5.0"
}

Status codes

CodeWhen
201Event created.
404account_id does not belong to the token user.
422Validation failed (missing campaign_id/contact_id/genre, or an invalid genre). Body: { "errors": { … } }.

Fetches a single event. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesEvent id (evt_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/events/9044

Response 200 OK — single event object (same fields as the index item above).

{
"id": 9044,
"account_id": 11,
"campaign_id": 42,
"contact_id": 880,
"genre": "opened",
"metadata": { "ip_address": "203.0.113.7" },
"created_at": "2026-05-30T09:58:21.000Z",
"updated_at": "2026-05-30T09:58:21.000Z",
"genre_display_name": "Opened",
"ip_address": "203.0.113.7"
}

Status codes

CodeWhen
200Event returned.
404No event with that id in an account the token user belongs to. Body: { "error": "Event not found" }.

Updates an event’s genre or metadata. Auth: Bearer; role: read (any role).

Parameters

Body params are wrapped in an event object; send only the fields you want to change.

NameInTypeRequiredDescription
idpathstringyesEvent id (evt_… or integer).
event.genrebodystringnoNew genre (one of the genre enum values; presence still enforced).
event.metadatabodyobjectnoReplacement metadata hash.
event.campaign_idbodyintegernoReassign campaign (presence enforced).
event.contact_idbodyintegernoReassign contact (presence enforced).
event.namebodystringnoAccepted but not persisted.

Request

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "event": { "metadata": { "note": "manual correction" } } }' \
https://platform.phishspot.com/api/v1/events/9044

Response 200 OK — the updated event object (same shape as show).

Status codes

CodeWhen
200Event updated.
404No event with that id in an account the token user belongs to. Body: { "error": "Event not found" }.
422Validation failed (e.g. invalid/blank genre). Body: { "errors": { … } }.

Permanently deletes an event. Auth: Bearer; role: read (any role).

No parameters beyond the bearer token and the path id.

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/events/9044

Response 204 No Content — empty body on success.

Status codes

CodeWhen
204Event deleted.
404No event with that id in an account the token user belongs to. Body: { "error": "Event not found" }.

Lists the account’s e-learning results newest-first. There are no query filters on this endpoint. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/accounts/11/results

Response 200 OK — JSON array of result objects.

FieldTypeDescription
idintegerResult id.
block_idintegerCourse block this result is for.
contact_idintegerContact who produced the result.
account_idintegerOwning account.
metadataobjectFree-form JSON (e.g. answer, correct, score, time_spent, completed). Defaults to {}.
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
blockobject | nullPresent when the block loads: { id, name }.
contactobject | nullPresent when the contact loads: { id, email, first_name, last_name, full_name }.
[
{
"id": 7100,
"block_id": 320,
"contact_id": 880,
"account_id": 11,
"metadata": { "answer": "B", "correct": true, "score": 100, "completed": true },
"created_at": "2026-05-31T14:20:00.000Z",
"updated_at": "2026-05-31T14:20:00.000Z",
"block": { "id": 320, "name": "Spot the Lookalike Domain" },
"contact": { "id": 880, "email": "jan.kowalski@acme.test", "first_name": "Jan", "last_name": "Kowalski", "full_name": "Jan Kowalski" }
}
]

Status codes

CodeWhen
200Results returned.
403Token user is not authorized to view the account (account.show? denied).
404account_id does not belong to the token user.

Records a contact’s result for a course block. The account is derived from the block automatically. Auth: Bearer; role: read (any role).

Parameters

Body params are wrapped in a result object.

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
result.block_idbodyintegeryesCourse block this result is for. Validated presence.
result.contact_idbodyintegeryesContact producing the result. Validated presence.
result.metadatabodyobjectnoJSON hash of answer/score/completion data. Defaults to {}. Permitted as a scalar param (:metadata), so send it as a JSON object value.
result.namebodystringnoAccepted by strong params but not persisted (no name column).
result.statebodystringnoAccepted by strong params but not persisted (Result has no state column/enum).

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "result": { "block_id": 320, "contact_id": 880, "metadata": { "answer": "B", "correct": true, "score": 100, "completed": true } } }' \
https://platform.phishspot.com/api/v1/accounts/11/results

Response 201 Created — the created result object (same shape as the index item above).

{
"id": 7150,
"block_id": 320,
"contact_id": 880,
"account_id": 11,
"metadata": { "answer": "B", "correct": true, "score": 100, "completed": true },
"created_at": "2026-06-02T08:30:00.000Z",
"updated_at": "2026-06-02T08:30:00.000Z",
"block": { "id": 320, "name": "Spot the Lookalike Domain" },
"contact": { "id": 880, "email": "jan.kowalski@acme.test", "first_name": "Jan", "last_name": "Kowalski", "full_name": "Jan Kowalski" }
}

Status codes

CodeWhen
201Result created.
404account_id does not belong to the token user.
422Validation failed (missing block_id/contact_id). Body: { "errors": { … } }.

Fetches a single result. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesResult id (res_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/results/7100

Response 200 OK — single result object (same fields as the index item above).

{
"id": 7100,
"block_id": 320,
"contact_id": 880,
"account_id": 11,
"metadata": { "answer": "B", "correct": true, "score": 100 },
"created_at": "2026-05-31T14:20:00.000Z",
"updated_at": "2026-05-31T14:20:00.000Z",
"block": { "id": 320, "name": "Spot the Lookalike Domain" },
"contact": { "id": 880, "email": "jan.kowalski@acme.test", "first_name": "Jan", "last_name": "Kowalski", "full_name": "Jan Kowalski" }
}

Status codes

CodeWhen
200Result returned.
404No result with that id in an account the token user belongs to.

Updates a result, typically to revise its metadata (answer, score, completion). Auth: Bearer; role: read (any role).

Parameters

Body params are wrapped in a result object; send only the fields you want to change.

NameInTypeRequiredDescription
idpathstringyesResult id (res_… or integer).
result.metadatabodyobjectnoReplacement metadata hash.
result.block_idbodyintegernoReassign block (presence enforced).
result.contact_idbodyintegernoReassign contact (presence enforced).
result.namebodystringnoAccepted but not persisted.
result.statebodystringnoAccepted but not persisted.

Request

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "result": { "metadata": { "answer": "C", "correct": false, "score": 0 } } }' \
https://platform.phishspot.com/api/v1/results/7100

Response 200 OK — the updated result object (same shape as show).

Status codes

CodeWhen
200Result updated.
404No result with that id in an account the token user belongs to.
422Validation failed (e.g. block_id/contact_id blanked). Body: { "errors": { … } }.

Permanently deletes a result. Auth: Bearer; role: read (any role).

No parameters beyond the bearer token and the path id.

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/results/7100

Response 204 No Content — empty body on success.

Status codes

CodeWhen
204Result deleted.
404No result with that id in an account the token user belongs to.

Aggregated phishing-susceptibility analytics for an account: one data point per delivered campaign over a date range, plus a roll-up summary. Backed by Campaigns::TrendService, which only counts campaigns whose state is in_progress or done (the delivered scope) created within the selected range.

Returns susceptibility metrics for an account’s delivered campaigns, ordered oldest-first, together with a summary (campaign count, average click rate, trend direction, and the most-clicked recipient group). Use it to power a trend dashboard or to track whether employees are getting better or worse at spotting phishing over time. Auth: Bearer; role: read (any role — any member of the account).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer). Must be an account the token’s user belongs to.
start_datequerystringnoStart of a custom range, YYYY-MM-DD. Parsed to the beginning of that day. If present (with or without end_date), the custom range takes precedence over range. When omitted but end_date is given, defaults to 1 year ago.
end_datequerystringnoEnd of a custom range, YYYY-MM-DD. Parsed to the end of that day. When omitted but start_date is given, defaults to now.
rangequerystringnoPreset range, used only when neither start_date nor end_date is present. One of 30d, 90d, 6m, all. Any other/absent value (including the default) yields the last 1 year. all spans from the Unix epoch to now.

Request

Terminal window
curl -X GET -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://platform.phishspot.com/api/v1/accounts/11/trends?start_date=2026-01-01&end_date=2026-06-01"

Response 200 OK — an object with a campaigns array (one entry per delivered campaign in range) and a summary object.

FieldTypeDescription
campaignsarrayDelivered campaigns in the range, ordered by created_at ascending. Each element is a data point (fields below). Empty array if none.
campaigns[].idintegerCampaign id (raw integer).
campaigns[].namestringCampaign name.
campaigns[].datestringDate the campaign ran, YYYY-MM-DD (ISO 8601 date). Uses scheduled_at if set, otherwise created_at.
campaigns[].open_ratenumber (float)Percent of recipients who opened the email (0.0 when no data).
campaigns[].click_ratenumber (float)Percent of recipients who clicked a link (0.0 when no data).
campaigns[].submit_ratenumber (float)Percent of recipients who submitted data on the landing page (0.0 when no data).
campaigns[].total_sentintegerNumber of recipients the campaign was sent to.
summaryobjectRoll-up across the campaigns above (fields below).
summary.total_campaignsintegerCount of delivered campaigns in the range (0 when none).
summary.avg_click_ratenumber (float)Mean of the per-campaign click_rate values, rounded to 1 decimal (0.0 when none).
summary.trend_directionstringOne of improving, worsening, stable, or neutral. Compares the average click rate of the most recent up-to-3 campaigns against the earliest up-to-3; neutral when fewer than 2 campaigns, stable when the change is within ±1.0 percentage point.
summary.most_vulnerable_groupstring | nullName of the recipient group with the highest click-to-sent ratio across the range; null when no group data is available.
{
"campaigns": [
{
"id": 42,
"name": "Q1 Invoice Lure",
"date": "2026-01-14",
"open_rate": 61.5,
"click_rate": 23.1,
"submit_rate": 7.7,
"total_sent": 130
},
{
"id": 57,
"name": "Password Expiry Notice",
"date": "2026-04-02",
"open_rate": 54.0,
"click_rate": 12.0,
"submit_rate": 4.0,
"total_sent": 150
}
],
"summary": {
"total_campaigns": 2,
"avg_click_rate": 17.6,
"trend_direction": "improving",
"most_vulnerable_group": "Finance"
}
}

Status codes

CodeWhen
200Trend data returned (the campaigns array and summary are present even when there are no matching campaigns).
404account_id does not belong to the token’s user ({"error":"Account not found"}).
422start_date or end_date is not a parseable YYYY-MM-DD date ({"error":"Invalid date; use YYYY-MM-DD."}).

E-learning courses delivered to employees who fall for a phishing simulation. A course is an ordered collection of blocks (text, HTML, video, quiz, etc.). Courses are either owned by your account or global (curated, shared library). Blocks are nested under a course for listing/creation but addressable by their own shallow id for show/update/destroy.

Lists every course available to the account — courses your account owns plus all global: true courses — ordered by name, with a lightweight block summary inlined. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer). Must be an account the token user belongs to.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/accounts/11/courses

Response 200 OK — JSON array of course objects (see the course fields below).

FieldTypeDescription
idintegerCourse id.
account_idintegerOwning account id.
namestringCourse name.
descriptionstringCourse description.
globalbooleantrue for shared/curated library courses, false for account-owned.
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
blocksarrayInlined block summaries (see fields below). Ordered as stored on the association.
blocks[].idintegerBlock id.
blocks[].namestringBlock name.
blocks[].orderintegerZero-based position within the course.
blocks[].genrestringBlock genre (see enum under block endpoints).
[
{
"id": 7,
"account_id": 11,
"name": "Spotting Spoofed Senders",
"description": "A short course on recognising display-name and domain spoofing.",
"global": false,
"created_at": "2026-05-01T09:12:00.000Z",
"updated_at": "2026-05-14T16:40:11.000Z",
"blocks": [
{ "id": 31, "name": "Intro", "order": 0, "genre": "html" },
{ "id": 32, "name": "Quick check", "order": 1, "genre": "quiz" }
]
}
]

Status codes

CodeWhen
200Courses returned.
404account_id is not an account the token user belongs to.

Creates a new account-owned course. The course is always created under the path account (global cannot be set — new courses are private). Both name and description are required by the model. Auth: Bearer; role: read (any role — all team members can create courses).

Parameters

Body params are wrapped in a course object.

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
coursebodyobjectyesWrapper object holding the fields below.
course.namebodystringyesCourse name. Presence-validated.
course.descriptionbodystringyesCourse description. Presence-validated.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "course": { "name": "Invoice Fraud 101", "description": "Recognising fake supplier invoices." } }' \
https://platform.phishspot.com/api/v1/accounts/11/courses

Response 201 Created — the created course, same shape as a list item (with an empty blocks array).

FieldTypeDescription
idintegerNew course id.
account_idintegerOwning account id (the path account).
namestringCourse name.
descriptionstringCourse description.
globalbooleanAlways false for newly created courses.
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
blocksarrayEmpty on creation.
{
"id": 19,
"account_id": 11,
"name": "Invoice Fraud 101",
"description": "Recognising fake supplier invoices.",
"global": false,
"created_at": "2026-06-02T10:00:00.000Z",
"updated_at": "2026-06-02T10:00:00.000Z",
"blocks": []
}

Status codes

CodeWhen
201Course created.
404account_id is not an account the token user belongs to.
422Validation failed — e.g. name or description blank. Body: {"errors": {"name": ["can't be blank"]}}.

Fetches a single course by its shallow id. Resolvable for courses your account owns or any global course; a course belonging to another tenant returns 404. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesCourse id (course_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/courses/7

Response 200 OK — one course object, same fields as a list item (including the inlined blocks summary).

FieldTypeDescription
idintegerCourse id.
account_idintegerOwning account id.
namestringCourse name.
descriptionstringCourse description.
globalbooleanWhether the course is from the shared library.
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
blocksarrayInlined block summaries: id, name, order, genre.
{
"id": 7,
"account_id": 11,
"name": "Spotting Spoofed Senders",
"description": "A short course on recognising display-name and domain spoofing.",
"global": false,
"created_at": "2026-05-01T09:12:00.000Z",
"updated_at": "2026-05-14T16:40:11.000Z",
"blocks": [
{ "id": 31, "name": "Intro", "order": 0, "genre": "html" },
{ "id": 32, "name": "Quick check", "order": 1, "genre": "quiz" }
]
}

Status codes

CodeWhen
200Course returned.
404No course with that id is owned by your account and it is not global.

Updates a course’s name and/or description. Auth: Bearer; role: read (any role), subject to the global-ownership and lock checks below.

Parameters

Body params are wrapped in a course object. Only name and description are permitted.

NameInTypeRequiredDescription
idpathstringyesCourse id (course_… or integer).
coursebodyobjectyesWrapper object holding the fields below.
course.namebodystringnoNew name. Cannot be blank if supplied (presence-validated).
course.descriptionbodystringnoNew description. Cannot be blank if supplied (presence-validated).

Request

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "course": { "description": "Updated for the 2026 supplier-fraud wave." } }' \
https://platform.phishspot.com/api/v1/courses/7

Response 200 OK — the updated course (same shape as GET /courses/:id).

{
"id": 7,
"account_id": 11,
"name": "Spotting Spoofed Senders",
"description": "Updated for the 2026 supplier-fraud wave.",
"global": false,
"created_at": "2026-05-01T09:12:00.000Z",
"updated_at": "2026-06-02T10:05:00.000Z",
"blocks": [
{ "id": 31, "name": "Intro", "order": 0, "genre": "html" }
]
}

Status codes

CodeWhen
200Course updated.
403The course is global and not owned by your account, or it is locked by an in-progress/paused campaign.
404Course not reachable by your account (not owned and not global).
422Validation failed — e.g. name/description set to blank. Body: {"errors": {...}}.

Deletes a course and (via dependent: :destroy) all of its blocks. Auth: Bearer; role: read (any role), subject to the global-ownership and lock checks below.

Parameters

No parameters beyond the bearer token and the path id.

NameInTypeRequiredDescription
idpathstringyesCourse id (course_… or integer).

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/courses/19

Response 204 No Content — empty body on success.

Status codes

CodeWhen
204Course deleted.
403The course is global and not owned by your account, or it is locked by an in-progress/paused campaign.
404Course not reachable by your account.

Lists the blocks of a course, ordered by order (ascending), scoped through Pundit to blocks of accessible (owned or global) courses. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
course_idpathstringyesCourse id (course_… or integer). Must be owned by your account or global.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/courses/7/blocks

Response 200 OK — JSON array of full block objects (fields below).

FieldTypeDescription
idintegerBlock id.
namestringBlock name.
course_idintegerParent course id.
orderintegerZero-based position within the course.
genrestringOne of: text, html, image, video, quiz, interactive, code, file_download.
metadataobjectFree-form JSON. For quiz blocks this holds the question/answers payload.
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
html_datastringRendered rich-text HTML. Present only when the block has ActionText content.
lockedbooleantrue when an associated campaign is in progress (block cannot be updated/deleted).
quiz_questionstringQuiz blocks only. The parsed question text.
quiz_answersarrayQuiz blocks only. Array of answer hashes parsed from metadata.
urlstringCanonical API URL for this block (/api/v1/blocks/:id).
[
{
"id": 31,
"name": "Intro",
"course_id": 7,
"order": 0,
"genre": "html",
"metadata": {},
"created_at": "2026-05-01T09:12:00.000Z",
"updated_at": "2026-05-01T09:12:00.000Z",
"html_data": "<div>Welcome to the course.</div>",
"locked": false,
"url": "https://platform.phishspot.com/api/v1/blocks/31"
},
{
"id": 32,
"name": "Quick check",
"course_id": 7,
"order": 1,
"genre": "quiz",
"metadata": [
{ "question_text": "Which sender is spoofed?" },
{ "answer_text": "no-reply@paypa1.com", "right_answer": "on" },
{ "answer_text": "no-reply@paypal.com" }
],
"created_at": "2026-05-01T09:13:00.000Z",
"updated_at": "2026-05-01T09:13:00.000Z",
"locked": false,
"quiz_question": "Which sender is spoofed?",
"quiz_answers": [
{ "answer_text": "no-reply@paypa1.com", "right_answer": "on" },
{ "answer_text": "no-reply@paypal.com" }
],
"url": "https://platform.phishspot.com/api/v1/blocks/32"
}
]

Status codes

CodeWhen
200Blocks returned (empty array if the course has none).
404course_id not reachable by your account (not owned and not global).

Adds a block to a course. The block’s account is set automatically from the course; if order is omitted it is auto-assigned to the end of the course. Content requirements vary by genre (see below). Auth: Bearer; role: read (any role), but you cannot add blocks to a global course you don’t own (403).

Parameters

Body params are wrapped in a block object.

NameInTypeRequiredDescription
course_idpathstringyesCourse id (course_… or integer).
blockbodyobjectyesWrapper object holding the fields below.
block.namebodystringyesBlock name. Presence-validated.
block.genrebodystringyesOne of text, html, image, video, quiz, interactive, code, file_download. Defaults to text if omitted.
block.bodybodystringconditionalPlain-text/markdown body. Required unless genre is quiz, html, video, or file_download. For video/file_download it serves as an optional description.
block.html_databodystringconditionalRich HTML content (ActionText). For html/text blocks, supply either body or html_data.
block.metadatabodyobject/arrayconditionalFree-form JSON. Required for quiz blocks, where it carries the question and answers (see quiz constraints).
block.orderbodyintegernoPosition within the course (integer ≥ 0). Auto-assigned to the end if omitted.
block.course_idbodyintegernoPermitted but normally redundant with the path course_id.
block.video_filebodyfileconditionalVideo attachment. Required for video blocks. Must be video/mp4 or video/webm, ≤ 300 MB, ≤ 1920×1080, ≤ 600 s, video codec h264/vp8/vp9, audio codec aac/opus. Blank string values are ignored.
block.document_filebodyfileconditionalDocument attachment. Required for file_download blocks. Any format, ≤ 100 MB. Blank string values are ignored.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"block": {
"name": "Identify the phish",
"genre": "quiz",
"metadata": [
{ "question_text": "Which sender is spoofed?" },
{ "answer_text": "no-reply@paypa1.com", "right_answer": "on" },
{ "answer_text": "no-reply@paypal.com" }
]
}
}' \
https://platform.phishspot.com/api/v1/courses/7/blocks

Response 201 Created — the created block (same shape as a list item).

{
"id": 33,
"name": "Identify the phish",
"course_id": 7,
"order": 2,
"genre": "quiz",
"metadata": [
{ "question_text": "Which sender is spoofed?" },
{ "answer_text": "no-reply@paypa1.com", "right_answer": "on" },
{ "answer_text": "no-reply@paypal.com" }
],
"created_at": "2026-06-02T10:10:00.000Z",
"updated_at": "2026-06-02T10:10:00.000Z",
"locked": false,
"quiz_question": "Which sender is spoofed?",
"quiz_answers": [
{ "answer_text": "no-reply@paypa1.com", "right_answer": "on" },
{ "answer_text": "no-reply@paypal.com" }
],
"url": "https://platform.phishspot.com/api/v1/blocks/33"
}

Status codes

CodeWhen
201Block created.
403The parent course is global and not owned by your account.
404course_id not reachable by your account.
422Validation failed — missing name/genre, missing required content for the genre (body, html_data, metadata, video_file, or document_file), too few/too many quiz answers, no correct quiz answer, or an unacceptable video/document file. Body: {"errors": {...}}.

Fetches a single block by its shallow id, scoped to blocks of courses your account can reach (owned or global). Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesBlock id (blk_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/blocks/32

Response 200 OK — one block object (full fields as in the blocks list).

FieldTypeDescription
idintegerBlock id.
namestringBlock name.
course_idintegerParent course id.
orderintegerZero-based position.
genrestringBlock genre (see enum above).
metadataobject/arrayFree-form JSON; quiz payload for quiz blocks.
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
html_datastringRendered rich text. Present only when set.
lockedbooleantrue when an associated campaign is in progress.
quiz_questionstringQuiz blocks only.
quiz_answersarrayQuiz blocks only.
urlstringCanonical API URL for this block.
{
"id": 32,
"name": "Quick check",
"course_id": 7,
"order": 1,
"genre": "quiz",
"metadata": [
{ "question_text": "Which sender is spoofed?" },
{ "answer_text": "no-reply@paypa1.com", "right_answer": "on" },
{ "answer_text": "no-reply@paypal.com" }
],
"created_at": "2026-05-01T09:13:00.000Z",
"updated_at": "2026-05-01T09:13:00.000Z",
"locked": false,
"quiz_question": "Which sender is spoofed?",
"quiz_answers": [
{ "answer_text": "no-reply@paypa1.com", "right_answer": "on" },
{ "answer_text": "no-reply@paypal.com" }
],
"url": "https://platform.phishspot.com/api/v1/blocks/32"
}

Status codes

CodeWhen
200Block returned.
404Block not reachable by your account (its course is neither owned nor global).

Updates a block. Same permitted fields and genre-specific content rules as create. Auth: Bearer; role: read (any role), but you cannot edit a block on a global course you don’t own (403), and a locked? block (campaign in progress) raises a not-destroyed error during the update.

Parameters

Body params are wrapped in a block object. Permitted keys: name, body, course_id, order, genre, metadata, html_data, video_file, document_file.

NameInTypeRequiredDescription
idpathstringyesBlock id (blk_… or integer).
blockbodyobjectyesWrapper object holding any of the permitted fields.
block.namebodystringnoNew name (presence-validated if supplied).
block.genrebodystringnoChange genre; same enum as create. Changing genre may make other fields required.
block.bodybodystringnoBody text. Required unless genre is quiz/html/video/file_download.
block.html_databodystringnoRich HTML content.
block.metadatabodyobject/arraynoFree-form JSON; quiz payload (2–6 answers, ≥1 correct).
block.orderbodyintegernoNew position (integer ≥ 0).
block.video_filebodyfilenoReplacement video (same constraints as create). Blank strings ignored.
block.document_filebodyfilenoReplacement document (≤ 100 MB). Blank strings ignored.

Request

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "block": { "name": "Identify the phishing sender", "order": 1 } }' \
https://platform.phishspot.com/api/v1/blocks/32

Response 200 OK — the updated block (same shape as GET /blocks/:id).

{
"id": 32,
"name": "Identify the phishing sender",
"course_id": 7,
"order": 1,
"genre": "quiz",
"metadata": [
{ "question_text": "Which sender is spoofed?" },
{ "answer_text": "no-reply@paypa1.com", "right_answer": "on" },
{ "answer_text": "no-reply@paypal.com" }
],
"created_at": "2026-05-01T09:13:00.000Z",
"updated_at": "2026-06-02T10:15:00.000Z",
"locked": false,
"quiz_question": "Which sender is spoofed?",
"quiz_answers": [
{ "answer_text": "no-reply@paypa1.com", "right_answer": "on" },
{ "answer_text": "no-reply@paypal.com" }
],
"url": "https://platform.phishspot.com/api/v1/blocks/32"
}

Status codes

CodeWhen
200Block updated.
403The block’s course is global and not owned by your account.
404Block not reachable by your account.
422Validation failed — blank name, missing genre-required content, invalid quiz answers, unacceptable file, or the block is locked by a campaign in progress. Body: {"errors": {...}}.

Deletes a block. The controller first checks whether the block is locked by an in-progress campaign and refuses with 422 if so. Auth: Bearer; role: read (any role), but you cannot delete a block on a global course you don’t own (403).

Parameters

No parameters beyond the bearer token and the path id.

NameInTypeRequiredDescription
idpathstringyesBlock id (blk_… or integer).

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/blocks/33

Response 204 No Content — empty body on success.

Status codes

CodeWhen
204Block deleted.
403The block’s course is global and not owned by your account.
404Block not reachable by your account.
422The block is locked (a connected campaign is in progress). Body: {"errors": ["Cannot delete block because connected campaign is in progress"]}.

Autopilots are recurring, hands-off phishing programs: you describe an audience and a cadence, and the platform keeps generating and delivering campaigns automatically until you stop it. An autopilot is created as a draft, then driven through a small lifecycle (draft → running ⇄ paused → stopped) via the dedicated member actions below.

::: caution Starting an autopilot activates a live, sending program — the platform will begin generating and delivering real phishing campaigns to the targeted members at the configured cadence. Treat POST /autopilots/:id/start as a go-live action, not a dry run. :::

A few model facts referenced throughout this section:

  • State (state): one of draft, running, paused, stopped.
  • Intensity period (intensity_period): one of day, week, month, year.
  • Duration kind (duration_kind): continuous (runs forever) or until_date (stops on ends_on).
  • End action type (end_action_type): one of nothing, redirect_to_course, message_page, redirect_to_url — controls what a target sees after the simulation.
  • Daily-rate cap: the effective send rate is intensity_count / period_in_days and must not exceed 2 campaigns/day (day=1, week=7, month=30, year=365 days per period). Exceeding it fails validation on intensity_count.
  • Editable: an autopilot is editable unless it is stopped. Once stopped, only read and delete remain available.

Lists every autopilot belonging to an account, newest first. Use it to render an autopilot dashboard or to find an autopilot’s id before acting on it. Auth: Bearer; role: read (any role — member, editor, or admin).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
statequerystringnoFilter to a single state. One of draft, running, paused, stopped. Any other value returns 422.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://platform.phishspot.com/api/v1/accounts/11/autopilots?state=running"

Response 200 OK — a JSON array of autopilot objects (each identical to the single-autopilot response below).

FieldTypeDescription
idintegerAutopilot id.
account_idintegerOwning account id.
namestringDisplay name.
statestringdraft | running | paused | stopped.
all_groupsbooleanWhether the autopilot targets all groups (vs. the listed groups).
intensity_countintegerCampaigns per intensity_period.
intensity_periodstringday | week | month | year.
duration_kindstringcontinuous | until_date.
ai_optimizer_enabledbooleanWhether AI optimization is on.
auto_include_new_membersbooleanWhether new members are auto-enrolled.
languagestringTarget language code (e.g. en, pl).
end_action_typestringnothing | redirect_to_course | message_page | redirect_to_url.
end_action_urlstring | nullRedirect URL (used when end_action_type is redirect_to_url).
created_atstringISO-8601 timestamp.
updated_atstringISO-8601 timestamp.
ends_onstring | nullISO-8601 date the program stops (when duration_kind is until_date), else null.
started_atstring | nullISO-8601 timestamp of first start, else null.
daily_ratenumberEffective campaigns/day, rounded to 2 decimals.
progress_percentageinteger | nullInteger % of expected campaigns delivered this period; null for draft/stopped.
course_idinteger | nullLinked e-learning course id, else null.
editablebooleanfalse only when state is stopped.
groupsarrayTargeted groups. Each: { "id": integer, "name": string }.
recent_campaignsarrayUp to 10 most recent generated campaigns, newest first. Each: { "id": integer, "name": string, "state": string }.
[
{
"id": 7,
"account_id": 11,
"name": "Finance team — quarterly drip",
"state": "running",
"all_groups": false,
"intensity_count": 2,
"intensity_period": "month",
"duration_kind": "continuous",
"ai_optimizer_enabled": true,
"auto_include_new_members": true,
"language": "en",
"end_action_type": "redirect_to_course",
"end_action_url": null,
"created_at": "2026-05-01T09:00:00Z",
"updated_at": "2026-06-01T12:30:00Z",
"ends_on": null,
"started_at": "2026-05-02T08:00:00Z",
"daily_rate": 0.07,
"progress_percentage": 88,
"course_id": 14,
"editable": true,
"groups": [
{ "id": 3, "name": "Finance" }
],
"recent_campaigns": [
{ "id": 102, "name": "Invoice approval — May", "state": "completed" }
]
}
]

Status codes

CodeWhen
200List returned (possibly empty).
404The account_id does not exist or the token’s user is not a member of it.
422state query param is present but not one of draft, running, paused, stopped.

Creates a new autopilot in the draft state. Blank fields are prefilled from the account’s autopilot settings and account defaults (industry, language, end-action URL/HTML, default course), so a minimal body still produces a usable draft. The autopilot is not started — call start afterward to go live. Auth: Bearer; role: admin/editor.

The body is wrapped in an autopilot object.

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
namebodystringyesDisplay name. Max 80 chars; must be unique within the account (case-insensitive).
all_groupsbodybooleannoTarget all groups. Defaults to true.
group_idsbodyarraynoPrefixed group ids (grp_…) to target. Unknown/foreign id → 422. Used when all_groups is false.
intensity_countbodyintegernoCampaigns per period. Must be ≥ 1. Defaults to 1. The resulting daily rate (intensity_count / period_days) must be ≤ 2/day.
intensity_periodbodystringnoday | week | month | year. Defaults to month.
duration_kindbodystringnocontinuous | until_date. Defaults to continuous.
ends_onbodystring (date)conditionalRequired (and must be a future date) when duration_kind is until_date.
ai_optimizer_enabledbodybooleannoDefaults to true.
auto_include_new_membersbodybooleannoDefaults to true.
languagebodystringnoTarget language code (e.g. en, pl). Prefilled from account settings/locale if blank.
industry_code_idbodyintegernoIndustry code id. Prefilled from account settings if blank.
end_action_typebodystringnonothing | redirect_to_course | message_page | redirect_to_url. Defaults to message_page.
end_action_urlbodystringconditionalRequired and must be http/https when end_action_type is redirect_to_url.
end_action_htmlbodystringconditionalRequired when end_action_type is message_page (prefilled from account defaults if blank).
course_idbodystringconditionalPrefixed course id (course_…); must belong to the account or be a global course (else 422). Required when end_action_type is redirect_to_course.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"autopilot": {
"name": "Finance team — quarterly drip",
"all_groups": false,
"group_ids": ["grp_3a9k"],
"intensity_count": 2,
"intensity_period": "month",
"duration_kind": "continuous",
"end_action_type": "redirect_to_course",
"course_id": "course_8h2d"
}
}' \
https://platform.phishspot.com/api/v1/accounts/11/autopilots

Response 201 Created — the newly created autopilot, in the same shape as the list item above (state will be draft, started_at and progress_percentage null).

{
"id": 9,
"account_id": 11,
"name": "Finance team — quarterly drip",
"state": "draft",
"all_groups": false,
"intensity_count": 2,
"intensity_period": "month",
"duration_kind": "continuous",
"ai_optimizer_enabled": true,
"auto_include_new_members": true,
"language": "en",
"end_action_type": "redirect_to_course",
"end_action_url": null,
"created_at": "2026-06-02T10:15:00Z",
"updated_at": "2026-06-02T10:15:00Z",
"ends_on": null,
"started_at": null,
"daily_rate": 0.07,
"progress_percentage": null,
"course_id": 14,
"editable": true,
"groups": [
{ "id": 3, "name": "Finance" }
],
"recent_campaigns": []
}

Status codes

CodeWhen
201Autopilot created.
400The autopilot body wrapper is missing entirely.
403Token’s user is a member (read-only) on the account — only admins/editors may create.
404The account_id does not exist or the token’s user is not a member of it.
422Validation failed — e.g. blank/duplicate/too-long name, daily rate above the 2/day cap, missing ends_on for until_date, missing end_action_url/end_action_html/course_id for the chosen end_action_type, or an unknown group_ids / course_id.

Fetches a single autopilot by its id. This is a shallow (non-nested) route — no account_id in the path; the account is inferred from the autopilot and the token’s membership is verified. Auth: Bearer; role: admin/editor.

No parameters beyond the bearer token.

NameInTypeRequiredDescription
idpathstringyesAutopilot id (auto_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/autopilots/9

Response 200 OK — a single autopilot object (identical field set to the list item documented under GET …/autopilots).

{
"id": 9,
"account_id": 11,
"name": "Finance team — quarterly drip",
"state": "running",
"all_groups": false,
"intensity_count": 2,
"intensity_period": "month",
"duration_kind": "continuous",
"ai_optimizer_enabled": true,
"auto_include_new_members": true,
"language": "en",
"end_action_type": "redirect_to_course",
"end_action_url": null,
"created_at": "2026-06-02T10:15:00Z",
"updated_at": "2026-06-02T10:16:00Z",
"ends_on": null,
"started_at": "2026-06-02T10:16:00Z",
"daily_rate": 0.07,
"progress_percentage": 100,
"course_id": 14,
"editable": true,
"groups": [
{ "id": 3, "name": "Finance" }
],
"recent_campaigns": []
}

Status codes

CodeWhen
200Autopilot returned.
403Token’s user is a member (read-only) on the autopilot’s account — single-autopilot reads require admin/editor.
404No autopilot with that id, or the token’s user has no active membership in its account.

Updates an autopilot’s configuration. The body is wrapped in an autopilot object and accepts the same fields as create. Passing group_ids replaces the full set of targeted groups. Auth: Bearer; role: admin/editor — and the autopilot must be editable (not stopped). Shallow route (no account_id).

Parameters

NameInTypeRequiredDescription
idpathstringyesAutopilot id (auto_… or integer).
namebodystringnoDisplay name. Max 80 chars; unique per account (case-insensitive).
all_groupsbodybooleannoTarget all groups.
group_idsbodyarraynoPrefixed group ids (grp_…). When present, replaces the current group set; unknown/foreign id → 422.
intensity_countbodyintegernoCampaigns per period (≥ 1; daily rate must stay ≤ 2/day).
intensity_periodbodystringnoday | week | month | year.
duration_kindbodystringnocontinuous | until_date.
ends_onbodystring (date)conditionalRequired future date when duration_kind is until_date.
ai_optimizer_enabledbodybooleannoToggle AI optimization.
auto_include_new_membersbodybooleannoToggle auto-enrollment.
languagebodystringnoTarget language code.
industry_code_idbodyintegernoIndustry code id.
end_action_typebodystringnonothing | redirect_to_course | message_page | redirect_to_url.
end_action_urlbodystringconditionalRequired http/https URL when end_action_type is redirect_to_url.
end_action_htmlbodystringconditionalRequired when end_action_type is message_page.
course_idbodystringconditionalPrefixed course id (course_…); must belong to the account or be global. Required when end_action_type is redirect_to_course.

Request

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "autopilot": { "intensity_count": 1, "intensity_period": "week" } }' \
https://platform.phishspot.com/api/v1/autopilots/9

Response 200 OK — the updated autopilot object (same shape as GET /autopilots/:id).

{
"id": 9,
"account_id": 11,
"name": "Finance team — quarterly drip",
"state": "running",
"all_groups": false,
"intensity_count": 1,
"intensity_period": "week",
"duration_kind": "continuous",
"ai_optimizer_enabled": true,
"auto_include_new_members": true,
"language": "en",
"end_action_type": "redirect_to_course",
"end_action_url": null,
"created_at": "2026-06-02T10:15:00Z",
"updated_at": "2026-06-02T11:00:00Z",
"ends_on": null,
"started_at": "2026-06-02T10:16:00Z",
"daily_rate": 0.14,
"progress_percentage": 100,
"course_id": 14,
"editable": true,
"groups": [
{ "id": 3, "name": "Finance" }
],
"recent_campaigns": []
}

Status codes

CodeWhen
200Autopilot updated.
400The autopilot body wrapper is missing entirely.
403Token’s user is a member (read-only), or the autopilot is stopped (stopped autopilots are read-only — delete only).
404No autopilot with that id, or the token’s user has no active membership in its account.
422Validation failed — same triggers as create (name, daily-rate cap, ends_on, end-action requirements, unknown group_ids/course_id).

Permanently deletes an autopilot and its group links; generated campaigns are detached (not deleted). A running autopilot cannot be deleted — stop it first. Auth: Bearer; role: admin/editor. Shallow route (no account_id).

No parameters beyond the bearer token.

NameInTypeRequiredDescription
idpathstringyesAutopilot id (auto_… or integer).

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/autopilots/9

Response 204 No Content — empty body on success.

Status codes

CodeWhen
204Autopilot deleted.
403Token’s user is a member (read-only) on the autopilot’s account.
404No autopilot with that id, or the token’s user has no active membership in its account.
422The autopilot is running — stop it before deleting. (A paused or draft autopilot can be deleted directly.)

Activates the autopilot: sets state to running (stamping started_at on first start) so the platform begins generating and delivering campaigns at the configured cadence. Use to go live, or to resume a paused autopilot. Auth: Bearer; role: admin/editor — and the autopilot must be editable (not stopped). Shallow route.

::: caution This is a live action: a successful call begins sending real simulated phishing email to targeted members. :::

No parameters beyond the bearer token.

NameInTypeRequiredDescription
idpathstringyesAutopilot id (auto_… or integer).

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/autopilots/9/start

Response 200 OK — the autopilot object with state: "running" (same shape as GET /autopilots/:id).

{
"id": 9,
"name": "Finance team — quarterly drip",
"state": "running",
"started_at": "2026-06-02T10:16:00Z",
"editable": true,
"progress_percentage": 100,
"daily_rate": 0.07
}

Status codes

CodeWhen
200Autopilot started/resumed; now running.
403Token’s user is a member (read-only), or the autopilot is stopped (a stopped autopilot cannot be restarted).
404No autopilot with that id, or the token’s user has no active membership in its account.

Pauses a running autopilot: sets state to paused so no new campaigns are generated. Resume later with start. Auth: Bearer; role: admin/editor — and the autopilot must be editable (not stopped). Shallow route.

No parameters beyond the bearer token.

NameInTypeRequiredDescription
idpathstringyesAutopilot id (auto_… or integer).

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/autopilots/9/pause

Response 200 OK — the autopilot object with state: "paused" (same shape as GET /autopilots/:id).

{
"id": 9,
"name": "Finance team — quarterly drip",
"state": "paused",
"started_at": "2026-06-02T10:16:00Z",
"editable": true,
"progress_percentage": 92
}

Status codes

CodeWhen
200Autopilot paused.
403Token’s user is a member (read-only), or the autopilot is stopped.
404No autopilot with that id, or the token’s user has no active membership in its account.

Stops the autopilot permanently: sets state to stopped. A stopped autopilot becomes read-only — it can no longer be updated, started, paused, or stopped again, only viewed or deleted. Auth: Bearer; role: admin/editor — and the autopilot must still be editable (not already stopped). Shallow route.

No parameters beyond the bearer token.

NameInTypeRequiredDescription
idpathstringyesAutopilot id (auto_… or integer).

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/autopilots/9/stop

Response 200 OK — the autopilot object with state: "stopped" and editable: false (same shape as GET /autopilots/:id).

{
"id": 9,
"name": "Finance team — quarterly drip",
"state": "stopped",
"started_at": "2026-06-02T10:16:00Z",
"editable": false,
"progress_percentage": null
}

Status codes

CodeWhen
200Autopilot stopped.
403Token’s user is a member (read-only), or the autopilot is already stopped.
404No autopilot with that id, or the token’s user has no active membership in its account.

Two related resources control which domains a campaign can send from:

  • Platform domains (pdm_…) are the attacker/landing domains PhishSpot operates. Most are platform-owned (“public” or assigned “private” domains); customers can also bring their own (BYOD) via provision_byod. Writing platform-domain records directly (POST/PATCH/DELETE /platform_domains) is reserved for admin users; provisioning a BYOD domain is open to any account member.
  • Secured domains (sdm_…) are DNS-ownership proofs. A customer adds a domain they control, drops a TXT record, and verifies it — this is how PhishSpot confirms a sender is allowed to send “from” that domain.

Lists every platform domain visible to the calling token’s account — all operational public domains plus any operational private domains assigned to the account. Results are ordered by name. Auth: Bearer; role: read (any role).

No parameters beyond the bearer token.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/platform_domains

Response 200 OK — a JSON array of platform-domain objects (each object uses the same fields as the show endpoint below).

FieldTypeDescription
idintegerNumeric id. (Use the pdm_… prefixed id in URLs.)
namestringDomain name (e.g. officelogin.in).
publicbooleanLegacy public flag from the column.
mailbooleanWhether this is the platform’s mail domain.
statestringLifecycle state. One of pending, checking, confirmed, purchasing, purchased, configuring_dns, dns_pending, configuring_postal, active, failed.
expires_onstring|nullISO8601 registration expiry, or null.
metadataobjectFree-form JSON (Cloudflare/Postal provisioning details, diagnostics, etc.).
created_atstringISO8601 timestamp.
updated_atstringISO8601 timestamp.
activebooleanTrue when state == "active".
byodbooleanTrue for customer “bring your own domain” records.
sending_blockedbooleanTrue when the domain is active but blocked from starting new sends.
nameserversarrayCloudflare nameservers assigned to this domain’s zone (empty unless captured).
cloudflare_errorstring|nullSet if the Cloudflare zone could not be created/read.
[
{
"id": 42,
"name": "officelogin.in",
"public": true,
"mail": false,
"state": "active",
"expires_on": "2027-03-01T00:00:00.000Z",
"metadata": {},
"created_at": "2026-01-10T09:00:00.000Z",
"updated_at": "2026-05-30T14:22:00.000Z",
"active": true,
"byod": false,
"sending_blocked": false,
"nameservers": [],
"cloudflare_error": null
}
]

Status codes

CodeWhen
200Domains returned (possibly an empty array).

Fetches a single platform domain by id. Useful for inspecting state, BYOD nameservers, and sending_blocked before selecting it for a campaign. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesPlatform-domain id (pdm_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/platform_domains/pdm_42

Response 200 OK — a single platform-domain object (same fields as the list endpoint above).

{
"id": 42,
"name": "officelogin.in",
"public": true,
"mail": false,
"state": "active",
"expires_on": "2027-03-01T00:00:00.000Z",
"metadata": {},
"created_at": "2026-01-10T09:00:00.000Z",
"updated_at": "2026-05-30T14:22:00.000Z",
"active": true,
"byod": false,
"sending_blocked": false,
"nameservers": [],
"cloudflare_error": null
}

Status codes

CodeWhen
200Domain found.
404No platform domain with that id.

Creates a platform-owned domain record directly. This is an administrative operation for managing the platform’s own domain pool — customers should use provision_byod instead. Auth: Bearer; role: admin.

Parameters — wrapped in a platform_domain object.

NameInTypeRequiredDescription
platform_domain.namebodystringyesDomain name. Lowercased/trimmed; must be a valid domain (3–253 chars, at least one dot, only a-z 0-9 . -, no leading/trailing dot or hyphen, no consecutive ../--, labels ≤63 chars). Must be globally unique (case-insensitive).
platform_domain.publicbodybooleannoLegacy public flag.
platform_domain.metadatabodyobjectnoFree-form JSON metadata.
platform_domain.expires_onbodystringnoISO8601 registration expiry.
`state`, `genre`, and `source` are not settable through the API; a record created here defaults to `state: "active"`, `genre: "public"`, `source: "manual"`.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "platform_domain": { "name": "secure-portal.co", "public": true } }' \
https://platform.phishspot.com/api/v1/platform_domains

Response 201 Created — the created platform-domain object (same fields as the show endpoint).

{
"id": 77,
"name": "secure-portal.co",
"public": true,
"mail": false,
"state": "active",
"expires_on": null,
"metadata": {},
"created_at": "2026-06-02T10:00:00.000Z",
"updated_at": "2026-06-02T10:00:00.000Z",
"active": true,
"byod": false,
"sending_blocked": false,
"nameservers": [],
"cloudflare_error": null
}

Status codes

CodeWhen
201Domain created.
403Caller is not an admin.
422Validation failed (e.g. blank/duplicate/invalid name). Body: { "errors": { "name": ["..."] } }.

Updates a platform-owned domain record. Auth: Bearer; role: admin.

Parameters — same wrapped platform_domain body as create; all fields optional.

NameInTypeRequiredDescription
idpathstringyesPlatform-domain id (pdm_… or integer).
platform_domain.namebodystringnoNew domain name (same validation rules as create).
platform_domain.publicbodybooleannoLegacy public flag.
platform_domain.metadatabodyobjectnoFree-form JSON metadata.
platform_domain.expires_onbodystringnoISO8601 registration expiry.
Renaming or otherwise updating a domain that has an in-progress (locked) campaign is rejected at the model level (`ActiveRecord::RecordNotDestroyed`), which surfaces as a 500-level error rather than 422. Avoid editing domains attached to running campaigns.

Request

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "platform_domain": { "expires_on": "2028-01-01T00:00:00Z" } }' \
https://platform.phishspot.com/api/v1/platform_domains/pdm_42

Response 200 OK — the updated platform-domain object (same fields as the show endpoint).

Status codes

CodeWhen
200Domain updated.
403Caller is not an admin.
404No platform domain with that id.
422Validation failed. Body: { "errors": { ... } }.

Deletes a platform-owned domain. A domain can only be deleted if it has no blocking associations: platform-owned domains must have no campaigns and no assigned accounts; BYOD domains must have no active (in-progress/paused) campaigns. Auth: Bearer; role: admin.

Parameters

NameInTypeRequiredDescription
idpathstringyesPlatform-domain id (pdm_… or integer).

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/platform_domains/pdm_42

Response 204 No Content — empty body on success.

Status codes

CodeWhen
204Domain deleted.
403Caller is not an admin.
404No platform domain with that id.
422Domain still has active campaigns (or other blocking associations). Body: { "error": "Cannot delete platform domain with active campaigns" }.

Re-checks the BYOD provisioning/health status of a domain the calling account owns, then returns the (reloaded) domain. Use this to poll a BYOD domain after delegating nameservers: an active domain runs a health check, a still-provisioning domain refreshes its setup status. Transient refresh failures are swallowed so polling never errors — you just get the last persisted state. Auth: Bearer; role: read (any role), but the token’s user must belong to the domain’s owner account.

Parameters

NameInTypeRequiredDescription
idpathstringyesPlatform-domain id (pdm_… or integer). Must be a BYOD domain owned by an account the caller belongs to.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/platform_domains/pdm_88/check

Response 200 OK — the refreshed platform-domain object (same fields as the show endpoint). Watch state, active, nameservers, sending_blocked, and cloudflare_error to track progress.

{
"id": 88,
"name": "mail.acme-customer.com",
"public": false,
"mail": false,
"state": "dns_pending",
"expires_on": null,
"metadata": { "cloudflare_nameservers": ["kara.ns.cloudflare.com", "rob.ns.cloudflare.com"] },
"created_at": "2026-06-01T08:00:00.000Z",
"updated_at": "2026-06-02T09:30:00.000Z",
"active": false,
"byod": true,
"sending_blocked": false,
"nameservers": ["kara.ns.cloudflare.com", "rob.ns.cloudflare.com"],
"cloudflare_error": null
}

Status codes

CodeWhen
200Status refreshed and domain returned.
404No such domain, or the domain has no owner account / the caller does not belong to its owner account.

POST /api/v1/accounts/:account_id/platform_domains/provision_byod

Section titled “POST /api/v1/accounts/:account_id/platform_domains/provision_byod”

Provisions a customer “bring your own domain” (BYOD) sending domain for the account. Creates a private, BYOD platform domain in a provisioning state and returns the Cloudflare nameservers the domain owner must set at their registrar; delegation and verification then complete asynchronously (poll with POST /platform_domains/:id/check). Idempotent — re-provisioning a domain the account already owns just re-surfaces its nameservers. Auth: Bearer; role: any role, but the caller must belong to account_id.

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer) the caller belongs to.
domain_namebodystringyesDomain to provision (e.g. mail.acme-customer.com). Lowercased/trimmed server-side. Not wrapped in any object — sent as a top-level key.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "domain_name": "mail.acme-customer.com" }' \
https://platform.phishspot.com/api/v1/accounts/11/platform_domains/provision_byod

Response 201 Created — the provisioned platform-domain object plus provisioning guidance. All platform-domain fields from the show endpoint, plus:

FieldTypeDescription
nameserversarrayCloudflare nameservers the domain owner must set at their registrar. (Also present in the base object; repeated at the top level for convenience.)
next_stepstringHuman-readable instruction describing the registrar change and the check endpoint to poll.
{
"id": 88,
"name": "mail.acme-customer.com",
"public": null,
"mail": false,
"state": "dns_pending",
"expires_on": null,
"metadata": { "cloudflare_nameservers": ["kara.ns.cloudflare.com", "rob.ns.cloudflare.com"] },
"created_at": "2026-06-02T09:00:00.000Z",
"updated_at": "2026-06-02T09:00:00.000Z",
"active": false,
"byod": true,
"sending_blocked": false,
"nameservers": ["kara.ns.cloudflare.com", "rob.ns.cloudflare.com"],
"cloudflare_error": null,
"next_step": "At the registrar for mail.acme-customer.com, replace the nameservers with the ones above, then poll POST /api/v1/platform_domains/pdm_88/check."
}

Status codes

CodeWhen
201Domain provisioned (or re-surfaced if already owned by this account).
403Caller does not belong to account_id (Pundit show? denied).
404account_id not found among the caller’s accounts. Body: { "error": "Account not found" }.
422Provisioning rejected. Body: { "error": "<message>" }, one of: blank name ("Domain name is required."), already registered by another account ("That domain is already registered in PhishSpot by another account."), or invalid domain ("That domain name is invalid.").

GET /api/v1/accounts/:account_id/secured_domains

Section titled “GET /api/v1/accounts/:account_id/secured_domains”

Lists the secured (ownership-verified) domains for an account, newest first. Optionally filter by verification state. Auth: Bearer; role: any role, but the caller must belong to account_id.

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer) the caller belongs to.
statequerystringnoFilter by verification state. One of pending, verified, failed. Omit for all.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://platform.phishspot.com/api/v1/accounts/11/secured_domains?state=verified"

Response 200 OK — a JSON array of secured-domain objects.

FieldTypeDescription
idintegerNumeric id. (Use the sdm_… prefixed id in URLs.)
account_idintegerOwning account id.
domainstringThe domain being verified (lowercased).
statestringVerification state: pending, verified, or failed.
verification_attemptsintegerNumber of DNS verification attempts made.
verified_atstring|nullISO8601 timestamp of successful verification, or null.
created_atstringISO8601 timestamp.
updated_atstringISO8601 timestamp.
dns_recordobjectThe TXT record the owner must publish to prove ownership.
dns_record.typestringAlways "TXT".
dns_record.namestringRecord host, e.g. _phishspot-verify.example.com.
dns_record.valuestringRecord value, e.g. phishspot-verify=<64-hex-token>.
[
{
"id": 5,
"account_id": 11,
"domain": "acme-customer.com",
"state": "verified",
"verification_attempts": 2,
"verified_at": "2026-05-20T11:00:00.000Z",
"created_at": "2026-05-19T16:30:00.000Z",
"updated_at": "2026-05-20T11:00:00.000Z",
"dns_record": {
"type": "TXT",
"name": "_phishspot-verify.acme-customer.com",
"value": "phishspot-verify=3f9a...c2"
}
}
]

Status codes

CodeWhen
200Domains returned (possibly an empty array).
403Caller does not belong to account_id.
404account_id not found among the caller’s accounts. Body: { "error": "Account not found" }.

POST /api/v1/accounts/:account_id/secured_domains

Section titled “POST /api/v1/accounts/:account_id/secured_domains”

Adds a domain the customer controls and returns the TXT record to publish for ownership verification. Public email-provider domains (e.g. gmail.com) are rejected. Auth: Bearer; role: any role, but the caller must belong to account_id.

Parameters — wrapped in a secured_domain object.

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer) the caller belongs to.
secured_domain.domainbodystringyesDomain to verify (e.g. acme-customer.com). Lowercased/trimmed; must match the standard domain format; must be unique per account (case-insensitive); cannot be a blocked public-email-provider domain.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "secured_domain": { "domain": "acme-customer.com" } }' \
https://platform.phishspot.com/api/v1/accounts/11/secured_domains

Response 201 Created — the created secured-domain object (same fields as the list endpoint), with state: "pending" and a freshly generated dns_record to publish.

{
"id": 6,
"account_id": 11,
"domain": "acme-customer.com",
"state": "pending",
"verification_attempts": 0,
"verified_at": null,
"created_at": "2026-06-02T10:00:00.000Z",
"updated_at": "2026-06-02T10:00:00.000Z",
"dns_record": {
"type": "TXT",
"name": "_phishspot-verify.acme-customer.com",
"value": "phishspot-verify=3f9a...c2"
}
}

Status codes

CodeWhen
201Domain added; publish the returned TXT record, then call verify_dns.
403Caller does not belong to account_id.
404account_id not found among the caller’s accounts. Body: { "error": "Account not found" }.
422Validation failed — blank/invalid format, duplicate for this account, or a blocked public-email-provider domain. Body: { "errors": { "domain": ["..."] } }.

Fetches a single secured domain by id, including the TXT record needed for verification. Shallow route (not nested under account) — the caller must belong to the owning account. Auth: Bearer; role: any role (account member).

Parameters

NameInTypeRequiredDescription
idpathstringyesSecured-domain id (sdm_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/secured_domains/sdm_6

Response 200 OK — a single secured-domain object (same fields as the list endpoint above).

Status codes

CodeWhen
200Domain found.
403Pundit denied (should not normally occur — the policy permits any member).
404No such domain, or the caller does not belong to its owning account.

Removes a secured domain. Blocked while any active (in-progress/paused) campaign sends from an email address on that domain. Shallow route — the caller must belong to the owning account. Auth: Bearer; role: any role (account member).

Parameters

NameInTypeRequiredDescription
idpathstringyesSecured-domain id (sdm_… or integer).

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/secured_domains/sdm_6

Response 204 No Content — empty body on success.

Status codes

CodeWhen
204Domain deleted.
403Pundit denied (should not normally occur).
404No such domain, or the caller does not belong to its owning account.
422Domain is verified and in use by active campaigns. Body: { "error": "Cannot delete secured domain <domain> while it is used by active campaigns: <campaign names>" }.

POST /api/v1/secured_domains/:id/verify_dns

Section titled “POST /api/v1/secured_domains/:id/verify_dns”

Runs DNS verification: looks up the expected TXT record for the domain and, if found, marks the domain verified. Call this after publishing the TXT record returned at creation. Returns the (reloaded) domain so you can read the resulting state. Shallow route — the caller must belong to the owning account. Auth: Bearer; role: any role (account member).

Parameters

NameInTypeRequiredDescription
idpathstringyesSecured-domain id (sdm_… or integer).

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/secured_domains/sdm_6/verify_dns

Response 200 OK — the refreshed secured-domain object (same fields as the list endpoint). On success state becomes "verified" and verified_at is set; otherwise it stays pending/failed and verification_attempts increments.

{
"id": 6,
"account_id": 11,
"domain": "acme-customer.com",
"state": "verified",
"verification_attempts": 1,
"verified_at": "2026-06-02T10:05:00.000Z",
"created_at": "2026-06-02T10:00:00.000Z",
"updated_at": "2026-06-02T10:05:00.000Z",
"dns_record": {
"type": "TXT",
"name": "_phishspot-verify.acme-customer.com",
"value": "phishspot-verify=3f9a...c2"
}
}

Status codes

CodeWhen
200Verification ran; check state in the response.
403Pundit denied (should not normally occur).
404No such domain, or the caller does not belong to its owning account.

Reported messages are suspicious emails that employees flagged — either forwarded into the account’s report inbox (source: inbound_webhook) or submitted through the Outlook add-in (source: outlook_addin). The read endpoints below return metadata only (sender, subject, received-at, source, reporter) — never the message body, headers, or attachments. They are scoped to accounts the calling token’s user belongs to.

GET /accounts/:account_id/reported_messages

Section titled “GET /accounts/:account_id/reported_messages”

Lists reported messages for one account, newest first (ordered by received_at descending). Use it to feed a triage queue or to sync reports into your SOC tooling. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer). Must be an account the token’s user belongs to.
sourcequerystringnoFilter by intake source. One of inbound_webhook, outlook_addin. An unknown value returns 422. Omit to return all sources.
limitqueryintegernoPage size. Defaults to 50; clamped to the range 1500 (values <1 or 0 fall back to 50, values >500 are capped at 500).
pagequeryintegerno1-based page number. Defaults to 1; values below 1 are treated as 1. Offset is computed as (page - 1) * limit.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://platform.phishspot.com/api/v1/accounts/11/reported_messages?source=outlook_addin&limit=25&page=1"

Response 200 OK — a JSON array of reported-message metadata objects (no envelope). Each element has these fields:

FieldTypeDescription
idintegerReported-message id.
account_idintegerOwning account id.
from_emailstringSender address of the reported email.
from_namestring | nullSender display name, if captured.
subjectstring | nullSubject line of the reported email.
message_idstring | nullOriginal RFC Message-ID, if captured.
sourcestringIntake source: inbound_webhook or outlook_addin.
received_atstring (ISO 8601)When the original email was received.
created_atstring (ISO 8601)When the report was ingested into PhishSpot.
from_domainstringLower-cased domain portion of from_email (text after @).
reporter_contact_emailstring | nullEmail of the account Contact who reported it (matched on from_email); null when no matching Contact exists.
[
{
"id": 4821,
"account_id": 11,
"from_email": "billing@suspicious-invoice.example",
"from_name": "Accounts Payable",
"subject": "Overdue invoice — action required",
"message_id": "<CADnf9x1@mail.suspicious-invoice.example>",
"source": "outlook_addin",
"received_at": "2026-05-28T09:14:00.000Z",
"created_at": "2026-05-28T09:15:32.000Z",
"from_domain": "suspicious-invoice.example",
"reporter_contact_email": "jane.doe@acme.test"
}
]

Status codes

CodeWhen
200Reports returned (array may be empty).
403The token’s user is authorized but Pundit denies AccountPolicy#show? for this account.
404account_id is not an account the token’s user belongs to (returns { "error": "Account not found" }).
422source is present but not one of the valid sources (returns { "error": "Unknown source …; valid sources: inbound_webhook, outlook_addin." }).

Fetches the metadata for a single reported message. This is a shallow route — it takes the report id directly, not an account_id path segment. Account isolation is enforced server-side: the lookup is scoped to the token user’s accounts, so requesting a report in another account returns 404. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesReported-message id (rep_… or integer).

No parameters beyond the bearer token and the path id.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/reported_messages/rep_4821

Response 200 OK — a single reported-message metadata object with the same fields as one element of the index array:

FieldTypeDescription
idintegerReported-message id.
account_idintegerOwning account id.
from_emailstringSender address of the reported email.
from_namestring | nullSender display name, if captured.
subjectstring | nullSubject line of the reported email.
message_idstring | nullOriginal RFC Message-ID, if captured.
sourcestringIntake source: inbound_webhook or outlook_addin.
received_atstring (ISO 8601)When the original email was received.
created_atstring (ISO 8601)When the report was ingested into PhishSpot.
from_domainstringLower-cased domain portion of from_email.
reporter_contact_emailstring | nullEmail of the matching account Contact, or null.
{
"id": 4821,
"account_id": 11,
"from_email": "billing@suspicious-invoice.example",
"from_name": "Accounts Payable",
"subject": "Overdue invoice — action required",
"message_id": "<CADnf9x1@mail.suspicious-invoice.example>",
"source": "outlook_addin",
"received_at": "2026-05-28T09:14:00.000Z",
"created_at": "2026-05-28T09:15:32.000Z",
"from_domain": "suspicious-invoice.example",
"reporter_contact_email": "jane.doe@acme.test"
}

Status codes

CodeWhen
200The report was found and returned.
404No report with that id exists within the token user’s accounts (returns { "error": "Resource not found" }).

POST /accounts/:account_id/reported_messages

Section titled “POST /accounts/:account_id/reported_messages”

Add-in only. Ingests a newly reported message from the Outlook add-in. This endpoint does not use a normal API bearer token or Pundit — it requires an add-in capability token (reported_messages:create) whose pinned account_id matches the :account_id in the URL. Standard integrations do not call this; reads use the two endpoints above. The body is wrapped in a reported_message object (permitted keys: from_email, from_name, subject, plain_body, html_body, received_at, message_id, headers, plus an attachments array) and a successful call returns 201 Created with { "id": "rep_…", "url": "…" }. A capability/account mismatch returns 403; validation failures return 422.

Status codes

CodeWhen
201The report was created.
403The token lacks the reported_messages:create capability, or its pinned account_id does not match the URL.
404account_id does not resolve to an existing account (returns { "error": "Account not found" }).
422The ingest service rejected the payload (returns { "error": "…" }).

The media library stores hosted image and CSS files for an account. Upload a file once, then reference its returned url inside campaign email HTML, landing pages, and templates. Always embed the hosted url — email clients (Gmail, Outlook) strip inline data: URIs, so base64-embedded images will not render.

Allowed content types: image/png, image/jpg, image/jpeg, image/gif, image/svg+xml, and text/css. Maximum file size is 5 MB. Every media item must have a unique (case-insensitive) name within its account.

All media endpoints are usable by any member of the account (no admin/editor gate). The collection endpoints (index, create) are nested under an account; the single-item endpoints (show, update, destroy) are shallow (/media/:id) and resolve only media belonging to one of the token user’s accounts — anything else returns 404.

Lists all media items for the account, newest first. Use it to find a file’s hosted url before embedding it in a campaign. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/accounts/11/media

Response 200 OK — a JSON array of media objects (each is the shape below).

FieldTypeDescription
idintegerMedia item id.
account_idintegerOwning account id.
namestringDisplay name (unique per account, case-insensitive).
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
urlstring|nullHosted file URL (relative path); embed this in HTML. null if no file is attached.
filenamestring|nullOriginal uploaded filename.
content_typestring|nullMIME type, e.g. image/png.
[
{
"id": 42,
"account_id": 11,
"name": "phishing-logo",
"created_at": "2026-06-02T10:15:00.000Z",
"updated_at": "2026-06-02T10:15:00.000Z",
"url": "/rails/active_storage/blobs/redirect/eyJfcmFpbHMi.../phishing-logo.png",
"filename": "phishing-logo.png",
"content_type": "image/png"
}
]

Status codes

CodeWhen
200Media list returned.
404The account_id is not one of the token user’s accounts.

Uploads a new file to the account’s media library. This request is multipart/form-data, not JSON — the file is sent as a form field, not a base64 string in a JSON body. Auth: Bearer; role: any account member.

Parameters (form fields, wrapped in a medium[...] object)

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
medium[name]bodystringyesDisplay name; must be unique (case-insensitive) within the account.
medium[attachment]bodyfileyesThe file to upload. Must be one of image/png, image/jpg, image/jpeg, image/gif, image/svg+xml, text/css, and ≤ 5 MB.

Request

Note the -F flags (multipart). Do not set Content-Type: application/json; let curl set the multipart boundary. The file is read from disk with @/path.

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
-F "medium[name]=phishing-logo" \
-F "medium[attachment]=@./phishing-logo.png;type=image/png" \
https://platform.phishspot.com/api/v1/accounts/11/media

Response 201 Created — the created media object (same shape as the list item above).

FieldTypeDescription
idintegerNew media item id.
account_idintegerOwning account id.
namestringDisplay name.
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
urlstringHosted file URL — embed this in campaign HTML.
filenamestringStored filename.
content_typestringMIME type of the uploaded file.
{
"id": 42,
"account_id": 11,
"name": "phishing-logo",
"created_at": "2026-06-02T10:15:00.000Z",
"updated_at": "2026-06-02T10:15:00.000Z",
"url": "/rails/active_storage/blobs/redirect/eyJfcmFpbHMi.../phishing-logo.png",
"filename": "phishing-logo.png",
"content_type": "image/png"
}

Status codes

CodeWhen
201Media created.
404The account_id is not one of the token user’s accounts.
422Validation failed — missing/blank name, duplicate name in the account, missing attachment, disallowed content type, or file over 5 MB. Body: { "errors": { … } }.

Example 422 body (missing attachment):

{ "errors": { "attachment": ["can't be blank"] } }

Returns a single media item by id. Resolves only media belonging to one of the token user’s accounts. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
idpathstringyesMedia item id (med_… or integer).

No parameters beyond the bearer token.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/media/42

Response 200 OK — the media object (same fields as the POST response above).

{
"id": 42,
"account_id": 11,
"name": "phishing-logo",
"created_at": "2026-06-02T10:15:00.000Z",
"updated_at": "2026-06-02T10:15:00.000Z",
"url": "/rails/active_storage/blobs/redirect/eyJfcmFpbHMi.../phishing-logo.png",
"filename": "phishing-logo.png",
"content_type": "image/png"
}

Status codes

CodeWhen
200Media item returned.
404No media item with that id belongs to one of the token user’s accounts.

Updates a media item. In practice only name is meaningful; you can also re-upload a file by sending a new attachment (multipart). Auth: Bearer; role: any account member.

Parameters (wrapped in a medium[...] object)

NameInTypeRequiredDescription
idpathstringyesMedia item id (med_… or integer).
medium[name]bodystringnoNew display name; must stay unique (case-insensitive) within the account.
medium[attachment]bodyfilenoReplacement file (send as multipart). Same content-type and 5 MB limits as create.

Request

A name-only change can be sent as JSON:

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "medium": { "name": "phishing-logo-v2" } }' \
https://platform.phishspot.com/api/v1/media/42

To replace the file, send multipart instead (-F "medium[attachment]=@./new.png;type=image/png").

Response 200 OK — the updated media object (same fields as show).

{
"id": 42,
"account_id": 11,
"name": "phishing-logo-v2",
"created_at": "2026-06-02T10:15:00.000Z",
"updated_at": "2026-06-02T11:02:00.000Z",
"url": "/rails/active_storage/blobs/redirect/eyJfcmFpbHMi.../phishing-logo.png",
"filename": "phishing-logo.png",
"content_type": "image/png"
}

Status codes

CodeWhen
200Media updated.
404No media item with that id belongs to one of the token user’s accounts.
422Validation failed — blank name, duplicate name, or (on file replacement) disallowed content type / over 5 MB. Body: { "errors": { … } }.

Permanently deletes a media item and its attached file. Any campaign HTML still referencing the file’s url will break, so remove references first. Auth: Bearer; role: any account member.

Parameters

NameInTypeRequiredDescription
idpathstringyesMedia item id (med_… or integer).

No parameters beyond the bearer token.

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/media/42

Response 204 No Content — empty body.

Status codes

CodeWhen
204Media deleted.
404No media item with that id belongs to one of the token user’s accounts.

Manage outbound webhook subscriptions and inspect the events PhishSpot has generated for an account. An endpoint is a URL you register to receive signed HTTP POSTs; an event is an immutable record of something that happened (a campaign was created, a contact was deleted, etc.) that is fanned out to every enabled endpoint subscribed to its type. For the delivery mechanics — retry schedule, the X-Webhook-Signature HMAC header, and how to verify it with the signing_secret — see Webhook delivery & signatures.

Available event_type_ids values. An endpoint subscribes by listing one or more of these strings. The same strings appear as the event_type on emitted events:

Event typeFires when
campaign.createdA campaign is created.
campaign.updatedA campaign is updated.
campaign.deletedA campaign is deleted.
contact.createdA contact is added.
contact.updatedA contact is updated.
contact.deletedA contact is removed.
deliverable.createdA campaign deliverable (one recipient’s send) is created.
deliverable.updatedA deliverable changes state (sent, opened, clicked, etc.).
spam_whitelist.updatedThe account’s spam/allow-list snapshot changes.

GET /accounts/:account_id/webhooks/endpoints

Section titled “GET /accounts/:account_id/webhooks/endpoints”

Lists every webhook endpoint registered on the account, newest first. Use this to render a management UI or reconcile your local config. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/accounts/11/webhooks/endpoints

Response 200 OK — a JSON array of endpoint objects. The signing_secret is omitted from this list view (it is only ever returned by the single-endpoint views).

FieldTypeDescription
idintegerEndpoint id. The path/show segment also accepts the whep_… prefixed form.
account_idintegerOwning account id.
namestringHuman label for the endpoint.
urlstringDestination URL that receives POSTs.
event_type_idsarray of stringSubscribed event types (see table above).
enabledbooleanWhether deliveries are currently being sent.
api_versionintegerPayload schema version (currently 1).
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
total_deliveriesintegerCount of all delivery records for this endpoint.
successful_deliveriesintegerCount of deliveries in delivered status.
failed_deliveriesintegerCount of deliveries in failed status.
[
{
"id": 42,
"account_id": 11,
"name": "Production listener",
"url": "https://hooks.example.com/phishspot",
"event_type_ids": ["campaign.created", "deliverable.updated"],
"enabled": true,
"api_version": 1,
"created_at": "2026-05-30T09:14:22Z",
"updated_at": "2026-06-01T12:03:10Z",
"total_deliveries": 128,
"successful_deliveries": 121,
"failed_deliveries": 7
}
]

Status codes

CodeWhen
200Endpoints returned (empty array if none).
403Caller is not a member of account_id.
404account_id does not exist or caller is not a member.

POST /accounts/:account_id/webhooks/endpoints

Section titled “POST /accounts/:account_id/webhooks/endpoints”

Registers a new webhook endpoint. The response includes the signing_secret exactly once — store it immediately, as it is needed to verify delivery signatures and is never shown in full again except on show/update/toggle of that same endpoint. Auth: Bearer; role: read (any role — membership in the account is sufficient).

Parameters

Body params are wrapped in a webhook_endpoint object.

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
webhook_endpoint.namebodystringyesHuman label. Presence-validated.
webhook_endpoint.urlbodystringyesDestination URL. Must be a valid http/https URL and pass the safety checks above.
webhook_endpoint.event_type_idsbodyarray of stringyesOne or more event types to subscribe to (see table). Presence-validated — at least one required.
webhook_endpoint.enabledbodybooleannoWhether to start enabled. Defaults to true.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"webhook_endpoint": {
"name": "Production listener",
"url": "https://hooks.example.com/phishspot",
"event_type_ids": ["campaign.created", "deliverable.updated"],
"enabled": false
}
}' \
https://platform.phishspot.com/api/v1/accounts/11/webhooks/endpoints

Response 201 Created — the created endpoint, including the one-time signing_secret. Fields are the same as the list view plus signing_secret:

FieldTypeDescription
idintegerEndpoint id.
account_idintegerOwning account id.
namestringHuman label.
urlstringDestination URL (trimmed/normalized).
event_type_idsarray of stringSubscribed event types.
enabledbooleanWhether deliveries are active.
api_versionintegerPayload schema version (1).
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
signing_secretstring64-char hex HMAC secret. Returned here — save it now.
total_deliveriesinteger0 for a new endpoint.
successful_deliveriesinteger0 for a new endpoint.
failed_deliveriesinteger0 for a new endpoint.
{
"id": 42,
"account_id": 11,
"name": "Production listener",
"url": "https://hooks.example.com/phishspot",
"event_type_ids": ["campaign.created", "deliverable.updated"],
"enabled": false,
"api_version": 1,
"created_at": "2026-06-02T10:00:00Z",
"updated_at": "2026-06-02T10:00:00Z",
"signing_secret": "9f2c1e7b4a6d8f0c3e5a7b9d1f3c5e7a9b1d3f5c7e9a1b3d5f7c9e1a3b5d7f9c",
"total_deliveries": 0,
"successful_deliveries": 0,
"failed_deliveries": 0
}

Status codes

CodeWhen
201Endpoint created.
403Caller is not a member of account_id.
404account_id does not exist or caller is not a member.
422Validation failed — missing name/url/event_type_ids, a malformed URL, or a URL that targets localhost / a private IP / a *.phishspot.com host.

Fetches a single endpoint, including its full signing_secret and lifetime delivery statistics. Use this to re-read the secret if you lost it, or to monitor delivery health. Auth: Bearer; role: read (any role — must be a member of the owning account).

Parameters

NameInTypeRequiredDescription
idpathstringyesEndpoint id (whep_… or integer).

No parameters beyond the bearer token and path id.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/webhooks/endpoints/whep_8xk2p9

Response 200 OK — one endpoint object with the same fields as the create response (includes signing_secret).

{
"id": 42,
"account_id": 11,
"name": "Production listener",
"url": "https://hooks.example.com/phishspot",
"event_type_ids": ["campaign.created", "deliverable.updated"],
"enabled": true,
"api_version": 1,
"created_at": "2026-05-30T09:14:22Z",
"updated_at": "2026-06-01T12:03:10Z",
"signing_secret": "9f2c1e7b4a6d8f0c3e5a7b9d1f3c5e7a9b1d3f5c7e9a1b3d5f7c9e1a3b5d7f9c",
"total_deliveries": 128,
"successful_deliveries": 121,
"failed_deliveries": 7
}

Status codes

CodeWhen
200Endpoint returned.
403Caller is not a member of the account that owns the endpoint.
404No endpoint with that id.

Updates an endpoint’s name, URL, subscribed event types, or enabled flag. The signing_secret is not regenerated and cannot be changed through this call. Auth: Bearer; role: read (any role — must be a member of the owning account).

Parameters

Body params are wrapped in a webhook_endpoint object; send only the fields you want to change.

NameInTypeRequiredDescription
idpathstringyesEndpoint id (whep_… or integer).
webhook_endpoint.namebodystringnoNew label. Cannot be blanked (presence-validated).
webhook_endpoint.urlbodystringnoNew destination URL. Re-validated for safety (same rules as create).
webhook_endpoint.event_type_idsbodyarray of stringnoReplacement set of subscribed event types. Cannot be emptied.
webhook_endpoint.enabledbodybooleannoEnable/disable.

Request

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"webhook_endpoint": {
"event_type_ids": ["campaign.created", "campaign.updated", "campaign.deleted"]
}
}' \
https://platform.phishspot.com/api/v1/webhooks/endpoints/whep_8xk2p9

Response 200 OK — the updated endpoint object (same fields as show, including signing_secret).

{
"id": 42,
"account_id": 11,
"name": "Production listener",
"url": "https://hooks.example.com/phishspot",
"event_type_ids": ["campaign.created", "campaign.updated", "campaign.deleted"],
"enabled": true,
"api_version": 1,
"created_at": "2026-05-30T09:14:22Z",
"updated_at": "2026-06-02T11:20:45Z",
"signing_secret": "9f2c1e7b4a6d8f0c3e5a7b9d1f3c5e7a9b1d3f5c7e9a1b3d5f7c9e1a3b5d7f9c",
"total_deliveries": 128,
"successful_deliveries": 121,
"failed_deliveries": 7
}

Status codes

CodeWhen
200Endpoint updated.
403Caller is not a member of the owning account.
404No endpoint with that id.
422Validation failed — blank name, empty event_type_ids, or an invalid/unsafe url.

Permanently deletes an endpoint and all of its delivery records. Events themselves are not deleted. Auth: Bearer; role: read (any role — must be a member of the owning account).

Parameters

NameInTypeRequiredDescription
idpathstringyesEndpoint id (whep_… or integer).

No parameters beyond the bearer token and path id.

Request

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/webhooks/endpoints/whep_8xk2p9

Response 204 No Content — empty body on success.

Status codes

CodeWhen
204Endpoint deleted.
403Caller is not a member of the owning account.
404No endpoint with that id.

Flips the endpoint’s enabled flag — enables a disabled endpoint or disables an enabled one. Use this to pause/resume deliveries without deleting the endpoint or losing its secret. Auth: Bearer; role: read (any role — must be a member of the owning account).

Parameters

NameInTypeRequiredDescription
idpathstringyesEndpoint id (whep_… or integer).

No parameters beyond the bearer token and path id — the new state is derived from the current one.

Request

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/webhooks/endpoints/whep_8xk2p9/toggle

Response 200 OK — the endpoint with its flipped enabled value (same fields as show, including signing_secret).

{
"id": 42,
"account_id": 11,
"name": "Production listener",
"url": "https://hooks.example.com/phishspot",
"event_type_ids": ["campaign.created", "deliverable.updated"],
"enabled": false,
"api_version": 1,
"created_at": "2026-05-30T09:14:22Z",
"updated_at": "2026-06-02T11:30:00Z",
"signing_secret": "9f2c1e7b4a6d8f0c3e5a7b9d1f3c5e7a9b1d3f5c7e9a1b3d5f7c9e1a3b5d7f9c",
"total_deliveries": 128,
"successful_deliveries": 121,
"failed_deliveries": 7
}

Status codes

CodeWhen
200State flipped; updated endpoint returned.
403Caller is not a member of the owning account.
404No endpoint with that id.

Lists the events generated for the account, newest first, paginated. Each event records what happened and bundles per-event delivery counts. Use this to audit what PhishSpot tried to send, independent of any one endpoint. Auth: Bearer; role: read (any role).

Parameters

NameInTypeRequiredDescription
account_idpathstringyesAccount id (acct_… or integer).
event_typequerystringnoFilter to a single event type (e.g. deliverable.updated). Omit for all types.
pagequeryintegernoPage number. Defaults to 1.
per_pagequeryintegernoItems per page. Defaults to 50.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://platform.phishspot.com/api/v1/accounts/11/webhooks/events?event_type=deliverable.updated&page=1&per_page=50"

Response 200 OK — a JSON array of event objects.

FieldTypeDescription
idintegerEvent id. The show segment also accepts the prefixed/raw form.
account_idintegerOwning account id.
subject_idintegerId of the record the event is about (campaign, contact, deliverable, …).
subject_typestringClass name of the subject (e.g. Campaign, Contact, Deliverable).
event_typestringOne of the event types in the table above.
api_versionintegerPayload schema version (1).
uuidstringStable unique id for this event (also used as payload.id).
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.
dataobjectEvent-specific data describing the change (shape varies by event_type).
payloadobjectThe exact JSON body delivered to endpoints (see below).
deliveriesobjectPer-event delivery counts.
deliveries.totalintegerAll delivery records for this event.
deliveries.deliveredintegerDeliveries in delivered status.
deliveries.failedintegerDeliveries in failed status.
deliveries.pendingintegerDeliveries in pending status.

The payload object is what receivers actually get, with this shape: id (the event uuid), type (the event_type), created_at (ISO 8601), data (same as the top-level data), and api_version.

[
{
"id": 9001,
"account_id": 11,
"subject_id": 305,
"subject_type": "Deliverable",
"event_type": "deliverable.updated",
"api_version": 1,
"uuid": "3f1c8a02-7d4e-4b9a-9c1e-2a6b5d8f0e11",
"created_at": "2026-06-02T09:58:12Z",
"updated_at": "2026-06-02T09:58:12Z",
"data": { "deliverable_id": "dlv_4k2m", "state": "clicked" },
"payload": {
"id": "3f1c8a02-7d4e-4b9a-9c1e-2a6b5d8f0e11",
"type": "deliverable.updated",
"created_at": "2026-06-02T09:58:12Z",
"data": { "deliverable_id": "dlv_4k2m", "state": "clicked" },
"api_version": 1
},
"deliveries": { "total": 2, "delivered": 1, "failed": 0, "pending": 1 }
}
]

Status codes

CodeWhen
200Events returned (empty array if none match).
403Caller is not a member of account_id.
404account_id does not exist or caller is not a member.

Fetches a single event by id, including its full data, the delivered payload, and delivery counts. Use this to inspect exactly what was sent for one occurrence. Auth: Bearer; role: read (any role — must be a member of the owning account).

Parameters

NameInTypeRequiredDescription
idpathstringyesEvent id (prefixed or raw integer).

No parameters beyond the bearer token and path id.

Request

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://platform.phishspot.com/api/v1/webhooks/events/9001

Response 200 OK — one event object, identical in shape to a single element of the index array above.

{
"id": 9001,
"account_id": 11,
"subject_id": 305,
"subject_type": "Deliverable",
"event_type": "deliverable.updated",
"api_version": 1,
"uuid": "3f1c8a02-7d4e-4b9a-9c1e-2a6b5d8f0e11",
"created_at": "2026-06-02T09:58:12Z",
"updated_at": "2026-06-02T09:58:12Z",
"data": { "deliverable_id": "dlv_4k2m", "state": "clicked" },
"payload": {
"id": "3f1c8a02-7d4e-4b9a-9c1e-2a6b5d8f0e11",
"type": "deliverable.updated",
"created_at": "2026-06-02T09:58:12Z",
"data": { "deliverable_id": "dlv_4k2m", "state": "clicked" },
"api_version": 1
},
"deliveries": { "total": 2, "delivered": 1, "failed": 0, "pending": 1 }
}

Status codes

CodeWhen
200Event returned.
403Caller is not a member of the account that owns the event.
404No event with that id.

Returns the current Outlook add-in release metadata. No authentication required. Cached ~5 minutes.

Parameters: none.

Terminal window
curl https://platform.phishspot.com/api/v1/outlook/version

Response 200 OK

FieldTypeDescription
lateststringLatest add-in version.
min_supportedstringOldest version still allowed to run.
bundle_filenamestringSideload bundle filename.
bundle_sha256stringSHA-256 of the bundle.
{ "latest": "1.1.0", "min_supported": "1.0.0", "bundle_filename": "phishspot-outlook-sideload-v1.1.0.zip", "bundle_sha256": "" }

Useful for inventory tooling that verifies which add-in version your fleet should run. See Chapter 20.

27.16 Spam-whitelist download (separate token system)

Section titled “27.16 Spam-whitelist download (separate token system)”

The mail-admin self-serve URL from Chapter 22 uses a different scheme — a 64-character token embedded in the path, no Authorization header:

NameInTypeRequiredDescription
tokenpathstring (64 hex)yesThe whitelist token from the Integrations panel.
formatpathenumnoOne of txt (default), json, csv, md, microsoft365, google-workspace, mimecast, proofpoint, postfix, spamassassin.

Possession of the URL is the only credential — treat it as a password and rotate it from Account settings → Integrations → Spam Filter Whitelist if it leaks.

Rack::Attack throttles apply per source IP for unauthenticated endpoints and per token for authenticated ones:

SurfaceLimit
Outlook pairing-code generation10 / minute / IP
Outlook pairing-code polling60 / minute / IP
Phishing report intake (add-in)30 / minute / IP
Spam whitelist download60 / minute / token

Exceeding a bucket returns 429 Too Many Requests with a Retry-After header. The general authenticated API isn’t otherwise rate-limited beyond infrastructure-level abuse protection; keep usage under a few hundred requests/minute/token or contact us to raise it.