Hook API v1.0.0

Read your organizations, phishing simulation results and training progress. Every count equals what Native Reports shows for the same scope. How v1 changes over time: https://api.hooksecurity.co/v1/docs#versioning

Getting started

An admin creates a key in Hook under Settings › API keys and copies it once. Send it on every request:

curl https://api.hooksecurity.co/v1/organizations \
  -H "Authorization: Bearer hook_live_…"

Keys never go in a URL; a key in a query string is refused. Revoking a key in Settings stops it on its next request. To try every route in the browser, open the API explorer. Counts and rates equal what Native Reports shows for the same organization and period.

What a key can read

Coverage is checked on every request against the current organization tree. Anything outside it returns the same 404 not_found as an ID that never existed. A key whose organization is suspended gets 403 organization_suspended.

Only Hook-native data is served. The native flags show whether an organization is on Hook-native simulations and training today; campaigns and enrollments it ran natively before switching off are still returned.

Paging

Lists return { "data": [...], "next_cursor": "..." }. Pass next_cursor back as cursor until it is null. Page size is limit, up to 500 (100 for campaigns and enrollment batches). Cursors are opaque, expire after 24 hours and only work with the same key and filters.

Per-person results have no change feed yet: re-read campaigns active in the last 30 days to pick up new clicks and reports.

Errors

Errors are application/problem+json (RFC 9457) with a stable code and a request_id to quote to support.

codeStatusMeaning
invalid_request400Invalid request
invalid_cursor400Invalid cursor
unsupported_parameter400Unsupported parameter
unauthorized401Unauthorized
key_revoked401API key revoked
forbidden403Forbidden
organization_suspended403Organization suspended
coverage_too_large403Coverage too large
not_found404Not found
method_not_allowed405Method not allowed
payload_too_large413Payload too large
rate_limited429Too many requests
internal_error500Internal error
service_unavailable503Service unavailable
query_timeout503Query timed out

Rate limits

Each key has a per-minute limit, enforced at Hook's edge in front of the API. Over it, requests get 429 rate_limited with a Retry-After header in seconds; wait that long before retrying.

AI assistants (MCP)

The API is also a Model Context Protocol server at /mcp (Streamable HTTP, stateless, JSON responses), with the same key and coverage. There is nothing to install: an AI client needs the URL and a key. The 10 read-only tools below return exactly what the matching REST endpoints return, and the AI model always runs on the client's side, never Hook's.

Supported protocol versions: 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07. Discovery: the server card (/mcp/server-card) and /.well-known/ai-catalog.json.

Connect

Claude Code, including the Code tab of the Claude desktop app. Run it once in a terminal, replacing the key; it is saved for every folder and survives restarts, and sessions started afterwards see it:

claude mcp add --scope user --transport http hook https://api.hooksecurity.co/mcp \
  --header "Authorization: Bearer hook_live_…"

Any other client (Cursor, VS Code, your own agent): URL https://api.hooksecurity.co/mcp, header Authorization: Bearer hook_live_…. To try the tools by hand, run the official MCP Inspector (npx @modelcontextprotocol/inspector) and connect with the same URL and header.

Names of organizations, campaigns, courses and people are customer-controlled text and are returned only as data. A tool error returns { "code", "title" } with the same codes as the REST errors below.

Tools

hook_whoami

Who am I. What this API key can read: its coverage and how many organizations it covers. Call first to orient.

No arguments.

hook_list_organizations

List organizations. Organizations this key covers, with type, state, active user count and whether they use Hook-native simulations and training.

ArgumentTypeDescription
limit optionalintegerPage size, 1 to 100. Defaults to 100.
cursor optionalstringnext_cursor from the previous call.

hook_get_organization

Get organization. One organization by ID.

ArgumentTypeDescription
organization_idstringOrganization ID (org_…)

hook_list_campaigns

List phishing campaigns. Phishing simulation campaigns with status, recipient counts, outcome counts and click and engagement rates, optionally for one organization.

ArgumentTypeDescription
organization_id optionalstringOnly this organization (org_…)
limit optionalintegerPage size, 1 to 100. Defaults to 100.
cursor optionalstringnext_cursor from the previous call.

hook_get_campaign

Get phishing campaign. One campaign's status and counts.

ArgumentTypeDescription
campaign_idstringCampaign ID (cmp_…)

hook_list_campaign_results

List campaign results per person. Who received a campaign and what they did: opened, clicked, landed, engaged, reported, with times. Filter by outcome, for example clicked, to list who clicked.

ArgumentTypeDescription
campaign_idstringCampaign ID (cmp_…)
outcome optional"clicked" | "reported" | "engaged" | "opened" | "delivered"
limit optionalintegerPage size, 1 to 100. Defaults to 100.
cursor optionalstringnext_cursor from the previous call.

hook_list_enrollment_batches

List training assignments. Training enrollment batches with course, due date and assigned, completed, outstanding and overdue counts.

ArgumentTypeDescription
organization_id optionalstringOnly this organization (org_…)
limit optionalintegerPage size, 1 to 100. Defaults to 100.
cursor optionalstringnext_cursor from the previous call.

hook_get_enrollment_batch

Get training assignment. One enrollment batch's course, due date and assigned, completed, outstanding and overdue counts.

ArgumentTypeDescription
batch_idstringEnrollment batch ID (eb_…)

hook_list_enrollments

List training progress per learner. One row per learner per assignment, with status, due date and whether it is overdue. Filter by organization, batch or status (overdue is a status filter).

ArgumentTypeDescription
organization_id optionalstringOnly this organization (org_…)
batch_id optionalstringOnly this batch (eb_…)
status optional"assigned" | "launched" | "in_progress" | "completed" | "failed" | "cancelled" | "expired" | "overdue"[]
limit optionalintegerPage size, 1 to 100. Defaults to 100.
cursor optionalstringnext_cursor from the previous call.

hook_list_billing_seats

List billable users per month. How many users to bill each client for in a month: the highest daily count of active people while the organization was active. Native Accounts only; organizations still on the legacy platform are not listed. days_inactive counts days the organization was suspended or expired, which are never billed. The current month is a figure so far (month_complete false).

ArgumentTypeDescription
month optionalstringCalendar month, UTC, as YYYY-MM. Defaults to the current month.
limit optionalintegerPage size, 1 to 100. Defaults to 100.
cursor optionalstringnext_cursor from the previous call.

Versioning and changes

This is version 1 of the API. Everything under /v1 and every MCP tool keeps working as documented here: v1 only grows.

What we add without notice

Build your client to ignore fields it does not know and to handle a value it does not recognise, for example by showing it as it is. A client generated from /v1/openapi.json does this by default: response objects are open, and extensible values are typed as strings.

What counts as breaking

Breaking changes come only in a new major version (/v2), which would run alongside v1. The one exception is a fix for a security or privacy defect, which we make in v1 and announce in the changelog.

Deprecation

Before anything is removed, it is marked deprecated in this reference and in the changelog, and responses that use it carry a Deprecation header (RFC 9745) and a Sunset header (RFC 8594) with the removal date. Removal comes at least 12 months after deprecation. An MCP tool name never changes meaning: a replacement tool gets a new name, and the old one keeps working until its sunset date.

Changelog

The version in this page's title and in /v1/openapi.json rises with every change: a minor version for additions, a patch for wording.

VersionDateChanges
1.0.02026-10-21First release: organizations, phishing simulation campaigns with per-person results, training enrollment batches and enrollments, and billable users per month.
MCP server at /mcp with the same data as read-only tools.

Support and request IDs

Every response carries an x-request-id header, and every error body a request_id. Quote it to Hook support: it finds the exact request in our logs. Hook logs which key, route or tool, outcome and timing for every call, never key secrets, people's names or emails, or tool arguments.

Reference

Generated from /v1/openapi.json (OpenAPI 3.1).

GET /v1/organizations

List organizations Every organization the key covers: an MSP's whole tree, or the key's own organization.

ParameterInTypeDescription
limit optionalqueryintegerPage size, 1 to 500. Defaults to 100.
cursor optionalquerystringThe next_cursor from the previous page.

200: A page of organizations. Each item in data:

FieldTypeDescription
idstringOrganization ID
namestring
type"msp" | "client" | …
parent_idstring | null
state"active" | "suspended" | …
active_user_countintegerPeople who are active, not deleted and receive simulations and training. Not the billing seat count.
nativeobject
native.simulationsbooleanOn Hook-native phishing simulations today. Campaigns it ran natively earlier are still listed when this is false.
native.trainingbooleanOn Hook-native training today. Enrollments it ran natively earlier are still listed when this is false.
native.accountbooleanA Native Account: created in Hook, not on the legacy platform. Only Native Accounts appear in billing seats.
created_atstringISO-8601 UTC timestamp
updated_atstringISO-8601 UTC timestamp

GET /v1/organizations/{id}

Get an organization

ParameterInTypeDescription
idpathstringOrganization ID

200: The organization.

FieldTypeDescription
idstringOrganization ID
namestring
type"msp" | "client" | …
parent_idstring | null
state"active" | "suspended" | …
active_user_countintegerPeople who are active, not deleted and receive simulations and training. Not the billing seat count.
nativeobject
native.simulationsbooleanOn Hook-native phishing simulations today. Campaigns it ran natively earlier are still listed when this is false.
native.trainingbooleanOn Hook-native training today. Enrollments it ran natively earlier are still listed when this is false.
native.accountbooleanA Native Account: created in Hook, not on the legacy platform. Only Native Accounts appear in billing seats.
created_atstringISO-8601 UTC timestamp
updated_atstringISO-8601 UTC timestamp

GET /v1/campaigns

List campaigns Hook-native phishing simulation campaigns in the key's coverage. Counts equal the Native Reports campaign report from the campaign's first send to today (UTC).

ParameterInTypeDescription
organization_id optionalquerystringOnly this organization's campaigns
limit optionalqueryintegerPage size, 1 to 100. Defaults to 100.
cursor optionalquerystringThe next_cursor from the previous page.

200: A page of campaigns. Each item in data:

FieldTypeDescription
idstringCampaign ID
organization_idstringOrganization ID
namestring
status"scheduled" | "starting" | "sending" | "sent" | "unconfirmed" | "processing" | "active" | "completed" | "partially_sent" | "failed" | "canceled" | "archived" | …Status of the campaign's most recent run, as on the Campaigns page.
status_group"active" | "scheduled" | "completed" | "failed" | … | nullThe Campaigns page filter the status counts under; null for statuses only shown under All.
launched_by"msp" | "hook" | "org" | …Who started it: the managing MSP, Hook (Campaign of the Month and Autopilot), or the organization itself.
scheduled_forstringWhen the most recent run was set to start.
run_countintegerLive runs. A relaunched campaign has more than one.
recipientsobject | nullNull when the campaign has over 50,000 recipient records, too many to count here; read its results instead.
outcomesobject | nullNull when recipients is null.
ratesobject | nullNull when recipients is null.
engagement_measuredbooleanFalse when the campaign's teaching page predates engagement measurement.
withheld_outcomes"reported" | …[]Outcomes hidden for this organization: reported, when reporting metrics are off or the report button is not live.
created_atstringISO-8601 UTC timestamp
updated_atstringISO-8601 UTC timestamp

GET /v1/campaigns/{id}

Get a campaign

ParameterInTypeDescription
idpathstringCampaign ID

200: The campaign.

FieldTypeDescription
idstringCampaign ID
organization_idstringOrganization ID
namestring
status"scheduled" | "starting" | "sending" | "sent" | "unconfirmed" | "processing" | "active" | "completed" | "partially_sent" | "failed" | "canceled" | "archived" | …Status of the campaign's most recent run, as on the Campaigns page.
status_group"active" | "scheduled" | "completed" | "failed" | … | nullThe Campaigns page filter the status counts under; null for statuses only shown under All.
launched_by"msp" | "hook" | "org" | …Who started it: the managing MSP, Hook (Campaign of the Month and Autopilot), or the organization itself.
scheduled_forstringWhen the most recent run was set to start.
run_countintegerLive runs. A relaunched campaign has more than one.
recipientsobject | nullNull when the campaign has over 50,000 recipient records, too many to count here; read its results instead.
outcomesobject | nullNull when recipients is null.
ratesobject | nullNull when recipients is null.
engagement_measuredbooleanFalse when the campaign's teaching page predates engagement measurement.
withheld_outcomes"reported" | …[]Outcomes hidden for this organization: reported, when reporting metrics are off or the report button is not live.
created_atstringISO-8601 UTC timestamp
updated_atstringISO-8601 UTC timestamp

GET /v1/campaigns/{id}/results

List a campaign's per-person results One row per recipient of the campaign's live runs. Re-read campaigns active in the last 30 days to pick up new outcomes; results have no change feed in v1.

ParameterInTypeDescription
idpathstringCampaign ID
outcome optionalquery"clicked" | "reported" | "engaged" | "opened" | "delivered"Only recipients with this outcome. delivered means the email was sent.
limit optionalqueryintegerPage size, 1 to 500. Defaults to 100.
cursor optionalquerystringThe next_cursor from the previous page.

200: A page of results. Each item in data:

FieldTypeDescription
idstringRecipient result ID
campaign_idstringCampaign ID
organization_idstringOrganization ID
personobject | nullNull when the person has since been removed.
delivery_status"scheduled" | "sent" | "failed" | "excluded" | …
sent_atstring | null
opened_atstring | null
clicked_atstring | nullFirst human click. Scanner and automated clicks are never credited.
landed_atstring | null
engaged_atstring | nullNull when not engaged or when engagement_measured is false.
reported_at optionalstring | nullAbsent when reported is listed in withheld_outcomes.
engagement_measuredboolean
withheld_outcomes"reported" | …[]
updated_atstringISO-8601 UTC timestamp

GET /v1/enrollment-batches

List enrollment batches Hook-native training assignments. Counts equal the Native Reports enrollment report from the batch's first assignment to today (UTC).

ParameterInTypeDescription
organization_id optionalquerystringOnly this organization's batches
limit optionalqueryintegerPage size, 1 to 100. Defaults to 100.
cursor optionalquerystringThe next_cursor from the previous page.

200: A page of enrollment batches. Each item in data:

FieldTypeDescription
idstringEnrollment batch ID
organization_idstringOrganization ID
namestring
courseobject
course.namestring
launched_by"msp" | "hook" | "org" | …Who started it: the managing MSP, Hook (Campaign of the Month and Autopilot), or the organization itself.
scheduled_forstring | null
due_atstring | nullThe batch due date. Each enrollment carries its own due_at.
countsobject | nullNull when the batch has over 50,000 enrollments, too many to count here; list its enrollments instead.
created_atstringISO-8601 UTC timestamp
updated_atstringISO-8601 UTC timestamp

GET /v1/enrollment-batches/{id}

Get an enrollment batch

ParameterInTypeDescription
idpathstringEnrollment batch ID

200: The batch.

FieldTypeDescription
idstringEnrollment batch ID
organization_idstringOrganization ID
namestring
courseobject
course.namestring
launched_by"msp" | "hook" | "org" | …Who started it: the managing MSP, Hook (Campaign of the Month and Autopilot), or the organization itself.
scheduled_forstring | null
due_atstring | nullThe batch due date. Each enrollment carries its own due_at.
countsobject | nullNull when the batch has over 50,000 enrollments, too many to count here; list its enrollments instead.
created_atstringISO-8601 UTC timestamp
updated_atstringISO-8601 UTC timestamp

GET /v1/enrollments

List enrollments One row per learner per batch, including cancelled enrollments.

ParameterInTypeDescription
organization_id optionalquerystringOnly this organization's enrollments
batch_id optionalquerystringOnly this batch's enrollments
status optionalquerystringComma-separated statuses; overdue is also accepted. A row matching any listed value is returned.
limit optionalqueryintegerPage size, 1 to 500. Defaults to 100.
cursor optionalquerystringThe next_cursor from the previous page.

200: A page of enrollments. Each item in data:

FieldTypeDescription
idstringEnrollment ID
batch_idstringEnrollment batch ID
organization_idstringOrganization ID
courseobject
course.namestring
personobject | nullNull when the learner has since been removed.
status"assigned" | "launched" | "in_progress" | "completed" | "failed" | "cancelled" | "expired" | …
overdueboolean
launched_by"msp" | "hook" | "org" | …Who started it: the managing MSP, Hook (Campaign of the Month and Autopilot), or the organization itself.
assigned_atstringISO-8601 UTC timestamp
due_atstring | null
started_atstring | null
completed_atstring | null
updated_atstringISO-8601 UTC timestamp

GET /v1/billing/seats

List billable users per month Billable users for each Native Account the key covers, for one calendar month: the highest daily count of active people on a day the organization was active, the same rule Hook's monthly usage report uses. Organizations still on the legacy platform are not listed; organization.native.account tells them apart. A client deleted during or after the month is still listed for it (organization_state deleted). Clients are grouped by today's organization tree: a client moved to another MSP is listed under its current MSP only. Without month, the current month so far; a cursor belongs to its month.

ParameterInTypeDescription
month optionalquerystringCalendar month, UTC (YYYY-MM). Defaults to the current month.
limit optionalqueryintegerPage size, 1 to 500. Defaults to 100.
cursor optionalquerystringThe next_cursor from the previous page.

200: A page of monthly billable users. Each item in data:

FieldTypeDescription
organization_idstringOrganization ID
organization_namestring
parent_idstring | null
organization_state"active" | "suspended" | "deleted" | …The organization today. A client deleted during or after the month still appears for that month, marked deleted.
monthstringCalendar month, UTC (YYYY-MM)
month_completebooleanFalse for the current month: the figures so far, which can still rise.
billable_usersinteger | nullThe highest active-user count recorded on any day of the month while the organization was active. Null when no day was recorded.
peak_datestring | nullThe day billable_users was recorded (the latest day on a tie).
purchased_seatsinteger | nullSeats the organization had on the peak day, when set.
days_recordedintegerDays in the month with a recorded count while the organization was active.
days_inactiveintegerDays in the month the organization was suspended or its account had expired. Recorded, but never billed.