Partner Portal API v1
The Partner Portal API provides programmatic access to your Holm Security portal data.
Supports MSSP reports and reseller reports with product usage data,
plus customer engagement metrics and churn risk indicators.
All responses are JSON. Dates use YYYY-MM-DD
format in the Europe/Stockholm timezone.
Base URL
https://portal-api.holmsecurity.com/v1
Content Type
All requests and responses use application/json; charset=utf-8.
Products
The API uses short product codes to identify Holm Security products:
| Code | Product |
|---|---|
SNS | System & Network Security |
WAS | Web Application Security |
PAT | Phishing Simulation & Awareness Training |
CS | Cloud Security (CSPM) |
DA | Device Agent (System & Network Security — Computers) |
Quick Start
Get up and running in minutes. Follow these steps to make your first API call.
Get your key pair
Log in to the Partner Portal, go to Settings. You will find two keys:
- Organizer Key (format
hsp_org_...) — always visible in the portal - API Key (format
hsp_...) — shown only once when generated. Click Generate API Key and copy it immediately.
Create a session
Use your key pair to create a session. The returned session token is used for all subsequent API calls:
curl -s -X POST https://portal-api.holmsecurity.com/v1/auth/session \
-H "Content-Type: application/json" \
-d '{"organizer_key": "hsp_org_your_organizer_key", "api_key": "hsp_your_api_key"}' | jq .import requests BASE_URL = "https://portal-api.holmsecurity.com/v1" # Create a session with your key pair session_resp = requests.post(f"{BASE_URL}/auth/session", json={ "organizer_key": "hsp_org_your_organizer_key", "api_key": "hsp_your_api_key" }) session = session_resp.json() SESSION_TOKEN = session["session_token"] print(f"Session created, expires at: {session['expires_at']}") # Use session token for all subsequent requests headers = {"Authorization": f"Session {SESSION_TOKEN}"}
const BASE_URL = "https://portal-api.holmsecurity.com/v1"; // Create a session with your key pair const sessionRes = await fetch(`${BASE_URL}/auth/session`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ organizer_key: "hsp_org_your_organizer_key", api_key: "hsp_your_api_key" }) }); const session = await sessionRes.json(); const SESSION_TOKEN = session.session_token; console.log(`Session created, expires at: ${session.expires_at}`); // Use session token for all subsequent requests const headers = { "Authorization": `Session ${SESSION_TOKEN}` };
Fetch usage data
Start by listing available reporting periods, then drill into peak usage or daily data:
# List available reporting periods curl -s "https://portal-api.holmsecurity.com/v1/mssp-report" \ -H "Authorization: Session pps_your_session_token" | jq . # Peak usage totals per product for February 2026 curl -s "https://portal-api.holmsecurity.com/v1/mssp-report/2026/02/usage/peaks" \ -H "Authorization: Session pps_your_session_token" | jq . # Per-company peaks curl -s "https://portal-api.holmsecurity.com/v1/mssp-report/2026/02/usage/peaks?group_by=company" \ -H "Authorization: Session pps_your_session_token" | jq . # Daily usage for a specific company curl -s "https://portal-api.holmsecurity.com/v1/mssp-report/2026/02/companies/SE-ARNXXXX/usage?view=daily" \ -H "Authorization: Session pps_your_session_token" | jq .
import requests, time # headers already set from step 2: {"Authorization": f"Session {SESSION_TOKEN}"} # List available reporting periods resp = requests.get(f"{BASE_URL}/mssp-report", headers=headers) periods = resp.json()["results"] for p in periods: print(f" {p['year']}-{p['period']} ({p['from']} to {p['to']})" f" {'(current)' if p['is_current'] else ''}") # Respect rate limit (1 req/sec) time.sleep(1) # Peak usage totals for the latest completed period period = next(p for p in periods if not p["is_partial"]) resp = requests.get( f"{BASE_URL}/mssp-report/{period['year']}/{period['period']}/usage/peaks", headers=headers) data = resp.json() for total in data["totals"]: print(f" {total['product']}: peak sum = {total['total_peak_sum']}")
// headers already set from step 2: { "Authorization": `Session ${SESSION_TOKEN}` } // Helper with rate-limit handling async function apiGet(path) { const res = await fetch(`${BASE_URL}${path}`, { headers }); if (res.status === 429) { const retryMs = (await res.json()).retry_after_ms || 1000; await new Promise(r => setTimeout(r, retryMs)); return apiGet(path); } return res.json(); } // List available periods const { results: periods } = await apiGet("/mssp-report"); periods.forEach(p => console.log(` ${p.year}-${p.period} (${p.from} to ${p.to})`) ); // Peak usage for the latest completed period const period = periods.find(p => !p.is_partial); const peaks = await apiGet( `/mssp-report/${period.year}/${period.period}/usage/peaks` ); peaks.totals.forEach(t => console.log(` ${t.product}: peak sum = ${t.total_peak_sum}`) );
429 responses with the retry_after_ms value.
Common Workflows
Generate a monthly billing report
Fetch peak usage for the previous reporting period to calculate billing totals:
# 1. Check which periods are available curl -s https://portal-api.holmsecurity.com/v1/mssp-report \ -H "Authorization: Session $SESSION_TOKEN" | jq . # 2. Get peak totals per product for the previous period (e.g. 2026-01) curl -s "https://portal-api.holmsecurity.com/v1/mssp-report/2026/01/usage/peaks" \ -H "Authorization: Session $SESSION_TOKEN" | jq . # 3. Get per-company peaks + totals in one call curl -s "https://portal-api.holmsecurity.com/v1/mssp-report/2026/01/usage/peaks?group_by=company,product" \ -H "Authorization: Session $SESSION_TOKEN" | jq .
import requests, time BASE_URL = "https://portal-api.holmsecurity.com/v1" SESSION_TOKEN = "pps_your_session_token" # from POST /v1/auth/session headers = {"Authorization": f"Session {SESSION_TOKEN}"} # 1. Find available periods periods = requests.get(f"{BASE_URL}/mssp-report", headers=headers).json()["results"] # Pick the most recent completed (non-partial) period bp = next(p for p in periods if not p["is_partial"]) print(f"Billing period: {bp['year']}-{bp['period']}") print(f" Range: {bp['from']} to {bp['to']}") time.sleep(1) # respect rate limit # 2. Fetch peak usage with both totals and per-company breakdown resp = requests.get( f"{BASE_URL}/mssp-report/{bp['year']}/{bp['period']}/usage/peaks", headers=headers, params={"group_by": "company,product"}) data = resp.json() # 3. Print billing summary print("\nBilling Summary:") for total in data["totals"]: print(f" {total['product']}: " f"peak sum = {total['total_peak_sum']} " f"across {total['company_count']} companies") # 4. Per-company breakdown print("\nPer-company peaks:") for company in data["results"]: for peak in company["peaks"]: val = peak["peak_value"] if peak["peak_value"] is not None else "N/A" print(f" {company['company_name']} ({company['security_center_id']}): " f"{peak['product']} = {val}")
Paginate through all companies in a period
Use limit and offset to iterate through large result sets:
import requests, time
def get_all_companies(base_url, headers, year, period):
"""Fetch all companies for a reporting period, handling pagination and rate limits."""
companies = []
offset = 0
limit = 100
while True:
resp = requests.get(
f"{base_url}/mssp-report/{year}/{period}/companies",
headers=headers, params={"limit": limit, "offset": offset})
if resp.status_code == 429:
retry_ms = resp.json().get("retry_after_ms", 1000)
time.sleep(retry_ms / 1000)
continue
data = resp.json()
companies.extend(data["results"])
if data["next"] is None:
break
offset += limit
time.sleep(1) # respect rate limit
return companies
companies = get_all_companies(BASE_URL, headers, 2026, "01")
print(f"Total companies: {len(companies)}")async function getAllCompanies(baseUrl, headers, year, period) {
const companies = [];
let offset = 0;
const limit = 100;
while (true) {
const url = `${baseUrl}/mssp-report/${year}/${period}/companies?limit=${limit}&offset=${offset}`;
const res = await fetch(url, { headers });
if (res.status === 429) {
const { retry_after_ms } = await res.json();
await new Promise(r => setTimeout(r, retry_after_ms || 1000));
continue;
}
const data = await res.json();
companies.push(...data.results);
if (!data.next) break;
offset += limit;
await new Promise(r => setTimeout(r, 1000)); // rate limit
}
return companies;
}
const companies = await getAllCompanies(BASE_URL, headers, 2026, "01");
console.log(`Total companies: ${companies.length}`);Export daily usage to CSV
Pull daily usage for a period and write it to a CSV file. The full usage endpoint paginates at company level, returning peaks + daily rows per company:
import requests, csv, time BASE_URL = "https://portal-api.holmsecurity.com/v1" SESSION_TOKEN = "pps_your_session_token" # from POST /v1/auth/session headers = {"Authorization": f"Session {SESSION_TOKEN}"} # Fetch full usage dump for January 2026 (paginated by company) all_rows = [] offset = 0 while True: resp = requests.get( f"{BASE_URL}/mssp-report/2026/01/usage", headers=headers, params={"limit": 100, "offset": offset}) if resp.status_code == 429: time.sleep(resp.json().get("retry_after_ms", 1000) / 1000) continue data = resp.json() # Each result is a company with peaks + daily arrays for company in data["results"]: for row in company["daily"]: all_rows.append({ "date": row["date"], "security_center_id": company["security_center_id"], "company_name": company["company_name"], "product": row["product"], "usage_value": row["usage_value"] }) if data["next"] is None: break offset += 100 time.sleep(1) # Write to CSV with open("usage_2026_01.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=[ "date", "security_center_id", "company_name", "product", "usage_value" ]) writer.writeheader() writer.writerows(all_rows) print(f"Exported {len(all_rows)} rows to usage_2026_01.csv")
Authentication
The API uses a key pair + session authentication system. Partners authenticate with two keys to create a short-lived session, then use the session token for all API calls.
Key Pair
Each partner organization receives two keys from the Partner Portal settings:
| Key | Format | Description |
|---|---|---|
| Organizer Key | hsp_org_... | Always visible in portal settings. Identifies your organization. |
| API Key | hsp_... | Shown only once when generated. Used together with the organizer key to create sessions. |
Authentication Flow
- Send a
POST /v1/auth/sessionrequest with yourorganizer_keyandapi_keyin the request body. - Receive a session token (format
pps_...) in the response. - Include the session token in all subsequent API requests using the
Authorizationheader:
Authorization: Session <session-token>
Session Properties
- Sessions are valid for 1 hour by default.
- Maximum 5 active sessions per organization. Exceeding this limit returns
409 Conflict. - Sessions are locked to the IP address that created them (dynamic origin binding). Requests from a different IP will be rejected.
Origin Restrictions
API keys can optionally have allowed origins (IP addresses or domains) configured in portal settings. When configured, session creation requests are only accepted from those origins. Sessions are always locked to the IP that created them, regardless of whether allowed origins are configured.
Scopes
Sessions inherit the scopes assigned to the API key. A session may have multiple scopes.
| Scope | Description |
|---|---|
me:read | Read identity endpoint |
mssp-report:read | MSSP report: billing periods (26th→25th), companies, and usage data |
reseller-report:read | Reseller report: calendar month periods (1st→last), companies, and usage data |
customers:read | Customer listing, engagement metrics, and churn risk indicators |
Error Responses
401 Unauthorized — Missing, invalid, or expired session token
{
"description": "Authentication credentials were not provided. Use Authorization: Session <token>"
}
403 Forbidden — Valid session but missing required scope, IP mismatch, or company not accessible
{
"description": "Permission denied",
"errors": {
"scope": ["Missing required scope: mssp-report:read"]
}
}
409 Conflict — Maximum sessions exceeded (when creating a new session)
{
"description": "Maximum of 5 active sessions per organization. Please invalidate an existing session (DELETE /v1/auth/session) or wait for one to expire.",
"active_sessions": 5,
"max_sessions": 5
}
Authentication
No session token required. Uses organizer key and API key in the request body.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
organizer_key | string | Yes | Your organizer key (format hsp_org_...) |
api_key | string | Yes | Your API key (format hsp_...) |
Example Request
curl -X POST https://portal-api.holmsecurity.com/v1/auth/session \
-H "Content-Type: application/json" \
-d '{
"organizer_key": "hsp_org_your_organizer_key",
"api_key": "hsp_your_api_key"
}'Response 201 Created
{
"session_token": "pps_abc123def456...",
"expires_at": "2026-03-11T15:30:00Z",
"valid_for_seconds": 3600,
"scopes": ["me:read", "mssp-report:read"],
"locked_to_origin": "203.0.113.42",
"message": "Use this session token in the Authorization header: Session <token>. This session is locked to origin 203.0.113.42."
}
If the request origin could not be determined, the response includes an origin_warning field instead of the locked origin message:
{
...
"locked_to_origin": null,
"message": "Use this session token in the Authorization header: Session <token>.",
"origin_warning": "Could not determine your request origin. This session is NOT locked to an IP and is therefore less secure. We strongly recommend configuring allowed origins on your API key in the partner portal."
}
Error Responses
401 Unauthorized — Invalid organizer key or API key
403 Forbidden — Request origin not in allowed origins list
409 Conflict — Maximum 5 active sessions per organization reached
Authentication
Accepts session token via Authorization: Session <token> header or session_token in the request body.
Example Request (header)
curl -X POST https://portal-api.holmsecurity.com/v1/auth/session/validate \ -H "Authorization: Session pps_your_session_token"
Example Request (body)
curl -X POST https://portal-api.holmsecurity.com/v1/auth/session/validate \
-H "Content-Type: application/json" \
-d '{"session_token": "pps_your_session_token"}'Response 200 OK — Valid session
{
"valid": true,
"expires_at": "2026-03-11T15:30:00Z",
"remaining_seconds": 2847,
"scopes": ["me:read", "mssp-report:read"],
"locked_to_origin": "203.0.113.42",
"created_at": "2026-03-11T14:30:00Z"
}
Response 200 OK — Expired session
{
"valid": false,
"reason": "Session has expired.",
"expired_at": "2026-03-11T15:30:00Z"
}
Response 200 OK — Not found
{
"valid": false,
"reason": "Session token not found."
}
Error Responses
400 Bad Request — No session token provided in header or body
Authentication
Requires Authorization: Session <token> header.
Example Request
curl -X DELETE https://portal-api.holmsecurity.com/v1/auth/session \ -H "Authorization: Session pps_your_session_token"
Response 200 OK
{
"success": true,
"message": "Session invalidated successfully."
}
Error Responses
401 Unauthorized — No session token provided or token is empty
404 Not Found — Session not found or already expired
Rate Limiting
The API enforces a limit of 1 request per second per session.
If exceeded, the API returns HTTP 429 Too Many Requests:
{
"description": "Rate limit exceeded",
"retry_after_ms": 750
}
Response headers include:
| Header | Description |
|---|---|
Retry-After | Seconds to wait (integer) |
X-Retry-After-Ms | Milliseconds to wait |
X-RateLimit-Limit | Always 1 |
X-RateLimit-Remaining | 0 when limited |
retry_after_ms then retry. Use exponential backoff if repeated 429s occur.
Pagination
List endpoints support offset-based pagination:
| Parameter | Default | Max |
|---|---|---|
limit | 100 | 1000 |
offset | 0 | — |
Response Envelope
{
"count": 0,
"next": null,
"previous": null,
"results": []
}
Error Format
All error responses follow a consistent structure:
{
"description": "Request failed",
"errors": {
"field": ["message"]
}
}
Required Scope
me:readExample Request
curl https://portal-api.holmsecurity.com/v1/me \ -H "Authorization: Session $SESSION_TOKEN"
Response
{
"partner_name": "Slate Rock and Gravel Co.",
"organizer_key": "hsp_org_abc123def456...",
"api_key_name": "General Access",
"api_key_prefix": "hsp_a1b2c3d4",
"scopes": ["me:read", "mssp-report:read"],
"allowed_origins": ["203.0.113.10"],
"timezone": "Europe/Stockholm"
}
MSSP Report
Usage reporting for MSSP partners. Billing periods run from the 26th of the previous month to the 25th (e.g. period 03 = Feb 26 → Mar 25). Companies are filtered by security center status and archived history. Reseller companies are excluded.
Required scope: mssp-report:read
Required Scope
mssp-report:readReporting Period Definition
Period MM covers the 26th of the previous month through the 25th of month MM (inclusive).
For the current period, to is capped at the latest available processed date.
Example Request
curl https://portal-api.holmsecurity.com/v1/mssp-report \ -H "Authorization: Session $SESSION_TOKEN"
Response
{
"timezone": "Europe/Stockholm",
"results": [
{
"year": 2026,
"period": "03",
"from": "2026-02-26",
"to": "2026-03-10",
"is_current": true,
"is_partial": true,
"url": "/v1/mssp-report/2026/03"
},
{
"year": 2026,
"period": "02",
"from": "2026-01-26",
"to": "2026-02-25",
"is_current": false,
"is_partial": false,
"url": "/v1/mssp-report/2026/02"
}
]
}
400.
Required Scope
mssp-report:readExample Request
curl https://portal-api.holmsecurity.com/v1/mssp-report/2026/02 \ -H "Authorization: Session $SESSION_TOKEN"
Response
{
"timezone": "Europe/Stockholm",
"year": 2026,
"period": "02",
"from": "2026-01-26",
"to": "2026-02-25",
"is_current": false,
"is_partial": false,
"eligible_company_count": 40,
"products": ["CS", "DA", "PAT", "SNS", "WAS"],
"links": {
"companies": "/v1/mssp-report/2026/02/companies",
"peaks": "/v1/mssp-report/2026/02/usage/peaks",
"usage": "/v1/mssp-report/2026/02/usage"
}
}
Required Scope
mssp-report:readQuery Parameters
| Name | Type | Description |
|---|---|---|
search | string | Filter by company name or security_center_id |
limit | integer | Pagination limit (default 100, max 1000) |
offset | integer | Pagination offset (default 0) |
Example Request
curl https://portal-api.holmsecurity.com/v1/mssp-report/2026/02/companies \ -H "Authorization: Session $SESSION_TOKEN"
Response
{
"reporting_period": {
"year": 2026, "period": "02",
"from": "2026-01-26", "to": "2026-02-25",
"is_partial": false
},
"count": 40,
"next": null,
"previous": null,
"results": [
{
"security_center_id": "SE-ARN1001",
"company_name": "Bedrock Security Inc.",
"status": "active",
"products": ["SNS", "WAS"]
}
]
}
Only companies that were active and not archived during the period are returned (resellers excluded).
Required Scope
mssp-report:readQuery Parameters
| Name | Type | Description |
|---|---|---|
view | string | peaks (default), daily, or all |
product | string | Filter by product code (e.g. SNS) |
limit | integer | Pagination limit for daily rows (default 100, max 1000) |
offset | integer | Pagination offset for daily rows (default 0) |
Example Request
curl "https://portal-api.holmsecurity.com/v1/mssp-report/2026/02/companies/SE-ARN1001/usage?view=peaks" \ -H "Authorization: Session $SESSION_TOKEN"
Response (view=peaks)
{
"reporting_period": {
"year": 2026, "period": "02",
"from": "2026-01-26", "to": "2026-02-25",
"is_partial": false
},
"company": {
"security_center_id": "SE-ARN1001",
"company_name": "Bedrock Security Inc."
},
"view": "peaks",
"usage": [
{ "product": "SNS", "peak_value": 142, "peak_date": "2026-02-15" },
{ "product": "WAS", "peak_value": 87, "peak_date": "2026-02-02" }
]
}
Response (view=daily)
Returns daily usage rows for the company, paginated.
Response (view=all)
Returns both peaks array and daily object (paginated) in a single response.
Required Scope
mssp-report:readQuery Parameters
| Name | Type | Description |
|---|---|---|
group_by | string | product (default), company, or company,product |
product | string | Filter by product code (e.g. SNS) |
limit | integer | Pagination limit (default 100, max 1000) |
offset | integer | Pagination offset (default 0) |
Example Requests
# Totals per product curl "https://portal-api.holmsecurity.com/v1/mssp-report/2026/02/usage/peaks" \ -H "Authorization: Session $SESSION_TOKEN" # Per-company breakdown curl "https://portal-api.holmsecurity.com/v1/mssp-report/2026/02/usage/peaks?group_by=company" \ -H "Authorization: Session $SESSION_TOKEN" # Both totals and per-company curl "https://portal-api.holmsecurity.com/v1/mssp-report/2026/02/usage/peaks?group_by=company,product" \ -H "Authorization: Session $SESSION_TOKEN"
Response (group_by=product)
Totals per product — sum of per-company peak values across all eligible companies.
{
"reporting_period": {
"year": 2026, "period": "02",
"from": "2026-01-26", "to": "2026-02-25",
"is_partial": false
},
"group_by": "product",
"eligible_company_count": 40,
"totals": [
{ "product": "SNS", "total_peak_sum": 2272, "company_count": 38, "null_company_count": 0 },
{ "product": "WAS", "total_peak_sum": 2, "company_count": 1, "null_company_count": 0 },
{ "product": "PAT", "total_peak_sum": 4121, "company_count": 35, "null_company_count": 0 }
]
}
Totals rule: total_peak_sum = sum of per-company peak values (nulls ignored).
null_company_count = companies where peak is null (product not enabled).
Response (group_by=company)
Per-company peak breakdown, paginated.
{
"reporting_period": { ... },
"group_by": "company",
"eligible_company_count": 40,
"count": 40,
"next": null,
"previous": null,
"results": [
{
"security_center_id": "SE-ARN1001",
"company_name": "Bedrock Security Inc.",
"peaks": [
{ "product": "SNS", "peak_value": 142, "peak_date": "2026-02-15" }
]
}
]
}
Response (group_by=company,product)
Both product totals AND per-company breakdown combined in one response.
product filter is set and a company does not have that product,
the company is still represented with peak_value: null and
null_reason: "product_not_enabled_for_company".
Required Scope
mssp-report:readQuery Parameters
| Name | Type | Description |
|---|---|---|
product | string | Filter by product code (e.g. SNS) |
limit | integer | Companies per page (default 100, max 1000) |
offset | integer | Company-level offset (default 0) |
Example Request
curl "https://portal-api.holmsecurity.com/v1/mssp-report/2026/02/usage" \ -H "Authorization: Session $SESSION_TOKEN"
Response
{
"reporting_period": {
"year": 2026, "period": "02",
"from": "2026-01-26", "to": "2026-02-25",
"is_partial": false
},
"eligible_company_count": 40,
"count": 40,
"next": null,
"previous": null,
"results": [
{
"security_center_id": "SE-ARN1001",
"company_name": "Bedrock Security Inc.",
"peaks": [
{ "product": "SNS", "peak_value": 142, "peak_date": "2026-02-15" }
],
"daily": [
{ "product": "SNS", "date": "2026-01-26", "usage_value": 130 },
{ "product": "SNS", "date": "2026-01-27", "usage_value": 135 }
]
}
]
}
Pagination is at the company level — a single company's data is never split across pages.
Use the product filter to reduce response size.
Reseller Report
Usage reporting for reseller partners. Periods use calendar months (1st to last day of month). Companies are included if their order status is active — no per-month historical eligibility check, no reseller exclusion.
Required scope: reseller-report:read
Required Scope
reseller-report:readPeriod Definition
Each period covers a calendar month: 1st to last day of the month. The -2 day date offset still applies to BO data timestamps.
Response
{
"timezone": "Europe/Stockholm",
"report_type": "reseller",
"results": [
{
"year": 2026,
"period": "03",
"from": "2026-03-01",
"to": "2026-03-10",
"is_current": true,
"is_partial": true,
"url": "/v1/reseller-report/2026/03"
}
]
}
Required Scope
reseller-report:readResponse
{
"timezone": "Europe/Stockholm",
"report_type": "reseller",
"year": 2026,
"period": "02",
"from": "2026-02-01",
"to": "2026-02-28",
"is_current": false,
"is_partial": false,
"eligible_company_count": 25,
"products": ["CS", "PAT", "SNS", "WAS"],
"links": {
"companies": "/v1/reseller-report/2026/02/companies",
"peaks": "/v1/reseller-report/2026/02/usage/peaks",
"usage": "/v1/reseller-report/2026/02/usage"
}
}
Required Scope
reseller-report:readQuery Parameters
| Name | Type | Description |
|---|---|---|
search | string | Filter by company name or security_center_id |
limit | integer | Pagination limit (default 100, max 1000) |
offset | integer | Pagination offset (default 0) |
Response
{
"reporting_period": { "year": 2026, "period": "02", ... },
"report_type": "reseller",
"count": 25,
"results": [
{
"security_center_id": "SE-ARN1001",
"company_name": "Rubble Construction Ltd.",
"order_status": "active",
"products": ["SNS", "WAS"]
}
]
}
Only companies with orderStatus === "active" are included. No per-month historical eligibility check.
Required Scope
reseller-report:readQuery Parameters
| Name | Type | Description |
|---|---|---|
view | string | peaks (default), daily, or all |
product | string | Filter by product code (e.g. SNS) |
limit | integer | Pagination limit for daily rows (default 100, max 1000) |
offset | integer | Pagination offset for daily rows (default 0) |
Response (view=peaks)
{
"reporting_period": { ... },
"report_type": "reseller",
"company": { "security_center_id": "SE-ARN1001", "company_name": "Rubble Construction Ltd." },
"view": "peaks",
"usage": [
{ "product": "SNS", "peak_value": 95, "peak_date": "2026-02-12" }
]
}
Required Scope
reseller-report:readQuery Parameters
| Name | Type | Description |
|---|---|---|
group_by | string | product (default), company, or company,product |
product | string | Filter by product code |
limit | integer | Pagination limit (default 100, max 1000) |
offset | integer | Pagination offset (default 0) |
Response (group_by=product)
{
"reporting_period": { ... },
"report_type": "reseller",
"group_by": "product",
"eligible_company_count": 25,
"totals": [
{ "product": "SNS", "total_peak_sum": 1850, "company_count": 22, "null_company_count": 0 }
]
}
Required Scope
reseller-report:readQuery Parameters
| Name | Type | Description |
|---|---|---|
product | string | Filter by product code |
limit | integer | Companies per page (default 100, max 1000) |
offset | integer | Company-level offset (default 0) |
Response
{
"reporting_period": { ... },
"report_type": "reseller",
"eligible_company_count": 25,
"count": 25,
"results": [
{
"security_center_id": "SE-ARN1001",
"company_name": "Rubble Construction Ltd.",
"peaks": [
{ "product": "SNS", "peak_value": 95, "peak_date": "2026-02-12" }
],
"daily": [
{ "product": "SNS", "date": "2026-02-01", "usage_value": 88 }
]
}
]
}
Customers
Customer endpoints provide access to active customers with engagement metrics and churn risk indicators.
Required scope: customers:read
The session's report scope determines whether MSSP or reseller eligibility logic is used: if the API key has mssp-report:read, the partner is an MSSP partner; if it has reseller-report:read, a reseller partner.
Active Customer Definition
- MSSP: active = SC status is active, not archived, resellers excluded
- Reseller: active =
orderStatus === "active"
Date Range Parameters
The from and to query parameters control the date range. Defaults to the last 30 days; maximum range is 12 months.
Date Offset
A −1 day offset is applied: data available in BO today is from yesterday.
Required Scope
customers:readQuery Parameters
| Name | Type | Description |
|---|---|---|
search | string | Filter by company name or security_center_id |
limit | integer | Pagination limit (default 100, max 1000) |
offset | integer | Pagination offset (default 0) |
Response
{
"count": 42,
"next": "/v1/customers?limit=100&offset=100",
"previous": null,
"results": [
{
"security_center_id": "SE-ARN1001",
"company_name": "Rubble Construction Ltd.",
"products": ["SNS", "WAS"],
"url": "/v1/customers/SE-ARN1001"
}
]
}
Required Scope
customers:readResponse
{
"security_center_id": "SE-ARN1001",
"company_name": "Rubble Construction Ltd.",
"products": ["SNS", "WAS"],
"links": {
"engagement": "/v1/customers/SE-ARN1001/engagement",
"churn": "/v1/customers/SE-ARN1001/churn"
}
}
Error Responses
403 Forbidden — Company not accessible with this session
{
"description": "Permission denied",
"errors": {
"security_center_id": ["Company not accessible with this session"]
}
}
404 Not Found — Company not found or not active
{
"description": "Company not found or not active.",
"errors": {
"security_center_id": ["Company is not currently active"]
}
}
Required Scope
customers:readQuery Parameters
| Name | Type | Description |
|---|---|---|
from | string | Start date YYYY-MM-DD (default: 30 days ago) |
to | string | End date YYYY-MM-DD (default: today) |
type | string | Comma-separated metric types: scan_activity, scanner_health, api_usage |
Response
{
"company": { "security_center_id": "SE-ARN1001", "company_name": "Rubble Construction Ltd." },
"from": "2026-02-10",
"to": "2026-03-11",
"metrics": {
"scan_activity": [
{
"date": "2026-02-10",
"sns_scans_succeeded": 12,
"sns_scans_failed": 1,
"was_scans_succeeded": 5,
"was_scans_failed": 0,
"cs_scans_succeeded": 3,
"cs_scans_failed": 0
}
],
"scanner_health": [
{ "date": "2026-02-10", "probes_active": 4, "probes_inactive": 1 }
],
"api_usage": [
{ "date": "2026-02-10", "api_calls": 87 }
]
}
}
Only days with non-zero data are returned. A −1 day offset is applied to all dates.
Required Scope
customers:readQuery Parameters
| Name | Type | Description |
|---|---|---|
from | string | Start date YYYY-MM-DD (default: 30 days ago) |
to | string | End date YYYY-MM-DD (default: today) |
type | string | Comma-separated indicator types: usage_trend, scan_failures, inactive_scanners, zero_activity |
Response
{
"company": { "security_center_id": "SE-ARN1001", "company_name": "Rubble Construction Ltd." },
"from": "2026-02-10",
"to": "2026-03-11",
"indicators": {
"usage_trend": [
{ "product": "SNS", "date": "2026-02-10", "usage_value": 88 }
],
"scan_failures": [
{ "date": "2026-02-10", "total_succeeded": 20, "total_failed": 3, "failure_ratio": 0.13 }
],
"inactive_scanners": [
{ "date": "2026-02-10", "probes_active": 4, "probes_inactive": 1 }
],
"zero_activity": [
{ "date": "2026-02-10", "has_scans": false, "has_api_calls": false }
]
}
}
A −1 day offset is applied to all dates.
Scope Requirements Summary
| Endpoint | Method | Required Scopes |
|---|---|---|
/v1/auth/session | POST | None (uses key pair) |
/v1/auth/session/validate | POST | Valid session |
/v1/auth/session | DELETE | Valid session |
/v1/me | GET | me:read |
/v1/mssp-report | GET | mssp-report:read |
/v1/mssp-report/{y}/{p} | GET | mssp-report:read |
/v1/mssp-report/{y}/{p}/companies | GET | mssp-report:read |
/v1/mssp-report/{y}/{p}/companies/{id}/usage | GET | mssp-report:read |
/v1/mssp-report/{y}/{p}/usage/peaks | GET | mssp-report:read |
/v1/mssp-report/{y}/{p}/usage | GET | mssp-report:read |
/v1/reseller-report | GET | reseller-report:read |
/v1/reseller-report/{y}/{p} | GET | reseller-report:read |
/v1/reseller-report/{y}/{p}/companies | GET | reseller-report:read |
/v1/reseller-report/{y}/{p}/companies/{id}/usage | GET | reseller-report:read |
/v1/reseller-report/{y}/{p}/usage/peaks | GET | reseller-report:read |
/v1/reseller-report/{y}/{p}/usage | GET | reseller-report:read |
/v1/customers | GET | customers:read |
/v1/customers/{scid} | GET | customers:read |
/v1/customers/{scid}/engagement | GET | customers:read |
/v1/customers/{scid}/churn | GET | customers:read |