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
- All clients: an MSP's key reads the MSP and every client in its tree, including clients added later.
- This organization: an organization's own key reads only that organization.
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.
| code | Status | Meaning |
|---|---|---|
invalid_request | 400 | Invalid request |
invalid_cursor | 400 | Invalid cursor |
unsupported_parameter | 400 | Unsupported parameter |
unauthorized | 401 | Unauthorized |
key_revoked | 401 | API key revoked |
forbidden | 403 | Forbidden |
organization_suspended | 403 | Organization suspended |
coverage_too_large | 403 | Coverage too large |
not_found | 404 | Not found |
method_not_allowed | 405 | Method not allowed |
payload_too_large | 413 | Payload too large |
rate_limited | 429 | Too many requests |
internal_error | 500 | Internal error |
service_unavailable | 503 | Service unavailable |
query_timeout | 503 | Query 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.
| Argument | Type | Description |
|---|---|---|
limit optional | integer | Page size, 1 to 100. Defaults to 100. |
cursor optional | string | next_cursor from the previous call. |
hook_get_organization
Get organization. One organization by ID.
| Argument | Type | Description |
|---|---|---|
organization_id | string | Organization 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.
| Argument | Type | Description |
|---|---|---|
organization_id optional | string | Only this organization (org_…) |
limit optional | integer | Page size, 1 to 100. Defaults to 100. |
cursor optional | string | next_cursor from the previous call. |
hook_get_campaign
Get phishing campaign. One campaign's status and counts.
| Argument | Type | Description |
|---|---|---|
campaign_id | string | Campaign 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.
| Argument | Type | Description |
|---|---|---|
campaign_id | string | Campaign ID (cmp_…) |
outcome optional | "clicked" | "reported" | "engaged" | "opened" | "delivered" | |
limit optional | integer | Page size, 1 to 100. Defaults to 100. |
cursor optional | string | next_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.
| Argument | Type | Description |
|---|---|---|
organization_id optional | string | Only this organization (org_…) |
limit optional | integer | Page size, 1 to 100. Defaults to 100. |
cursor optional | string | next_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.
| Argument | Type | Description |
|---|---|---|
batch_id | string | Enrollment 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).
| Argument | Type | Description |
|---|---|---|
organization_id optional | string | Only this organization (org_…) |
batch_id optional | string | Only this batch (eb_…) |
status optional | "assigned" | "launched" | "in_progress" | "completed" | "failed" | "cancelled" | "expired" | "overdue"[] | |
limit optional | integer | Page size, 1 to 100. Defaults to 100. |
cursor optional | string | next_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).
| Argument | Type | Description |
|---|---|---|
month optional | string | Calendar month, UTC, as YYYY-MM. Defaults to the current month. |
limit optional | integer | Page size, 1 to 100. Defaults to 100. |
cursor optional | string | next_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
- New endpoints and MCP tools.
- New optional query parameters and tool arguments.
- New fields in responses and tool results.
- New values in fields listed as extensible (
x-extensible-enumin the OpenAPI document), such as a new campaign status. - New error codes.
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
- Removing or renaming an endpoint, tool, field or value.
- Changing a field's type or meaning.
- Making an optional parameter required, or refusing input we accept today.
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.
| Version | Date | Changes |
|---|---|---|
1.0.0 | 2026-10-21 | First 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.
| Parameter | In | Type | Description |
|---|---|---|---|
limit optional | query | integer | Page size, 1 to 500. Defaults to 100. |
cursor optional | query | string | The next_cursor from the previous page. |
200: A page of organizations. Each item in data:
| Field | Type | Description |
|---|---|---|
id | string | Organization ID |
name | string | |
type | "msp" | "client" | … | |
parent_id | string | null | |
state | "active" | "suspended" | … | |
active_user_count | integer | People who are active, not deleted and receive simulations and training. Not the billing seat count. |
native | object | |
native.simulations | boolean | On Hook-native phishing simulations today. Campaigns it ran natively earlier are still listed when this is false. |
native.training | boolean | On Hook-native training today. Enrollments it ran natively earlier are still listed when this is false. |
native.account | boolean | A Native Account: created in Hook, not on the legacy platform. Only Native Accounts appear in billing seats. |
created_at | string | ISO-8601 UTC timestamp |
updated_at | string | ISO-8601 UTC timestamp |
GET /v1/organizations/{id}
Get an organization
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string | Organization ID |
200: The organization.
| Field | Type | Description |
|---|---|---|
id | string | Organization ID |
name | string | |
type | "msp" | "client" | … | |
parent_id | string | null | |
state | "active" | "suspended" | … | |
active_user_count | integer | People who are active, not deleted and receive simulations and training. Not the billing seat count. |
native | object | |
native.simulations | boolean | On Hook-native phishing simulations today. Campaigns it ran natively earlier are still listed when this is false. |
native.training | boolean | On Hook-native training today. Enrollments it ran natively earlier are still listed when this is false. |
native.account | boolean | A Native Account: created in Hook, not on the legacy platform. Only Native Accounts appear in billing seats. |
created_at | string | ISO-8601 UTC timestamp |
updated_at | string | ISO-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).
| Parameter | In | Type | Description |
|---|---|---|---|
organization_id optional | query | string | Only this organization's campaigns |
limit optional | query | integer | Page size, 1 to 100. Defaults to 100. |
cursor optional | query | string | The next_cursor from the previous page. |
200: A page of campaigns. Each item in data:
| Field | Type | Description |
|---|---|---|
id | string | Campaign ID |
organization_id | string | Organization ID |
name | string | |
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" | … | null | The 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_for | string | When the most recent run was set to start. |
run_count | integer | Live runs. A relaunched campaign has more than one. |
recipients | object | null | Null when the campaign has over 50,000 recipient records, too many to count here; read its results instead. |
outcomes | object | null | Null when recipients is null. |
rates | object | null | Null when recipients is null. |
engagement_measured | boolean | False 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_at | string | ISO-8601 UTC timestamp |
updated_at | string | ISO-8601 UTC timestamp |
GET /v1/campaigns/{id}
Get a campaign
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string | Campaign ID |
200: The campaign.
| Field | Type | Description |
|---|---|---|
id | string | Campaign ID |
organization_id | string | Organization ID |
name | string | |
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" | … | null | The 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_for | string | When the most recent run was set to start. |
run_count | integer | Live runs. A relaunched campaign has more than one. |
recipients | object | null | Null when the campaign has over 50,000 recipient records, too many to count here; read its results instead. |
outcomes | object | null | Null when recipients is null. |
rates | object | null | Null when recipients is null. |
engagement_measured | boolean | False 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_at | string | ISO-8601 UTC timestamp |
updated_at | string | ISO-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.
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string | Campaign ID |
outcome optional | query | "clicked" | "reported" | "engaged" | "opened" | "delivered" | Only recipients with this outcome. delivered means the email was sent. |
limit optional | query | integer | Page size, 1 to 500. Defaults to 100. |
cursor optional | query | string | The next_cursor from the previous page. |
200: A page of results. Each item in data:
| Field | Type | Description |
|---|---|---|
id | string | Recipient result ID |
campaign_id | string | Campaign ID |
organization_id | string | Organization ID |
person | object | null | Null when the person has since been removed. |
delivery_status | "scheduled" | "sent" | "failed" | "excluded" | … | |
sent_at | string | null | |
opened_at | string | null | |
clicked_at | string | null | First human click. Scanner and automated clicks are never credited. |
landed_at | string | null | |
engaged_at | string | null | Null when not engaged or when engagement_measured is false. |
reported_at optional | string | null | Absent when reported is listed in withheld_outcomes. |
engagement_measured | boolean | |
withheld_outcomes | "reported" | …[] | |
updated_at | string | ISO-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).
| Parameter | In | Type | Description |
|---|---|---|---|
organization_id optional | query | string | Only this organization's batches |
limit optional | query | integer | Page size, 1 to 100. Defaults to 100. |
cursor optional | query | string | The next_cursor from the previous page. |
200: A page of enrollment batches. Each item in data:
| Field | Type | Description |
|---|---|---|
id | string | Enrollment batch ID |
organization_id | string | Organization ID |
name | string | |
course | object | |
course.name | string | |
launched_by | "msp" | "hook" | "org" | … | Who started it: the managing MSP, Hook (Campaign of the Month and Autopilot), or the organization itself. |
scheduled_for | string | null | |
due_at | string | null | The batch due date. Each enrollment carries its own due_at. |
counts | object | null | Null when the batch has over 50,000 enrollments, too many to count here; list its enrollments instead. |
created_at | string | ISO-8601 UTC timestamp |
updated_at | string | ISO-8601 UTC timestamp |
GET /v1/enrollment-batches/{id}
Get an enrollment batch
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string | Enrollment batch ID |
200: The batch.
| Field | Type | Description |
|---|---|---|
id | string | Enrollment batch ID |
organization_id | string | Organization ID |
name | string | |
course | object | |
course.name | string | |
launched_by | "msp" | "hook" | "org" | … | Who started it: the managing MSP, Hook (Campaign of the Month and Autopilot), or the organization itself. |
scheduled_for | string | null | |
due_at | string | null | The batch due date. Each enrollment carries its own due_at. |
counts | object | null | Null when the batch has over 50,000 enrollments, too many to count here; list its enrollments instead. |
created_at | string | ISO-8601 UTC timestamp |
updated_at | string | ISO-8601 UTC timestamp |
GET /v1/enrollments
List enrollments One row per learner per batch, including cancelled enrollments.
| Parameter | In | Type | Description |
|---|---|---|---|
organization_id optional | query | string | Only this organization's enrollments |
batch_id optional | query | string | Only this batch's enrollments |
status optional | query | string | Comma-separated statuses; overdue is also accepted. A row matching any listed value is returned. |
limit optional | query | integer | Page size, 1 to 500. Defaults to 100. |
cursor optional | query | string | The next_cursor from the previous page. |
200: A page of enrollments. Each item in data:
| Field | Type | Description |
|---|---|---|
id | string | Enrollment ID |
batch_id | string | Enrollment batch ID |
organization_id | string | Organization ID |
course | object | |
course.name | string | |
person | object | null | Null when the learner has since been removed. |
status | "assigned" | "launched" | "in_progress" | "completed" | "failed" | "cancelled" | "expired" | … | |
overdue | boolean | |
launched_by | "msp" | "hook" | "org" | … | Who started it: the managing MSP, Hook (Campaign of the Month and Autopilot), or the organization itself. |
assigned_at | string | ISO-8601 UTC timestamp |
due_at | string | null | |
started_at | string | null | |
completed_at | string | null | |
updated_at | string | ISO-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.
| Parameter | In | Type | Description |
|---|---|---|---|
month optional | query | string | Calendar month, UTC (YYYY-MM). Defaults to the current month. |
limit optional | query | integer | Page size, 1 to 500. Defaults to 100. |
cursor optional | query | string | The next_cursor from the previous page. |
200: A page of monthly billable users. Each item in data:
| Field | Type | Description |
|---|---|---|
organization_id | string | Organization ID |
organization_name | string | |
parent_id | string | null | |
organization_state | "active" | "suspended" | "deleted" | … | The organization today. A client deleted during or after the month still appears for that month, marked deleted. |
month | string | Calendar month, UTC (YYYY-MM) |
month_complete | boolean | False for the current month: the figures so far, which can still rise. |
billable_users | integer | null | The highest active-user count recorded on any day of the month while the organization was active. Null when no day was recorded. |
peak_date | string | null | The day billable_users was recorded (the latest day on a tie). |
purchased_seats | integer | null | Seats the organization had on the peak day, when set. |
days_recorded | integer | Days in the month with a recorded count while the organization was active. |
days_inactive | integer | Days in the month the organization was suspended or its account had expired. Recorded, but never billed. |