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:

CodeProduct
SNSSystem & Network Security
WASWeb Application Security
PATPhishing Simulation & Awareness Training
CSCloud Security (CSPM)
DADevice Agent (System & Network Security — Computers)

Quick Start

Get up and running in minutes. Follow these steps to make your first API call.

1

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.
2

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}` };
3

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}`)
);
Tip: The API is rate-limited to 1 request per second. Add a 1-second delay between calls, or handle 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:

KeyFormatDescription
Organizer Keyhsp_org_...Always visible in portal settings. Identifies your organization.
API Keyhsp_...Shown only once when generated. Used together with the organizer key to create sessions.

Authentication Flow

  1. Send a POST /v1/auth/session request with your organizer_key and api_key in the request body.
  2. Receive a session token (format pps_...) in the response.
  3. Include the session token in all subsequent API requests using the Authorization header:
Authorization: Session <session-token>

Session Properties

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.

ScopeDescription
me:readRead identity endpoint
mssp-report:readMSSP report: billing periods (26th→25th), companies, and usage data
reseller-report:readReseller report: calendar month periods (1st→last), companies, and usage data
customers:readCustomer 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
}
POST /v1/auth/session Create a new session

Authentication

No session token required. Uses organizer key and API key in the request body.

Request Body

FieldTypeRequiredDescription
organizer_keystringYesYour organizer key (format hsp_org_...)
api_keystringYesYour 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

POST /v1/auth/session/validate Check if a session is valid

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

DELETE /v1/auth/session Invalidate a session

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:

HeaderDescription
Retry-AfterSeconds to wait (integer)
X-Retry-After-MsMilliseconds to wait
X-RateLimit-LimitAlways 1
X-RateLimit-Remaining0 when limited
Wait retry_after_ms then retry. Use exponential backoff if repeated 429s occur.

Error Format

All error responses follow a consistent structure:

{
  "description": "Request failed",
  "errors": {
    "field": ["message"]
  }
}
GET /v1/me Partner identity, API key details, and scopes

Required Scope

me:read

Example 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

GET /v1/mssp-report List available reporting periods

Required Scope

mssp-report:read

Reporting 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"
    }
  ]
}
Up to 6 reporting periods are available (current + 5 previous), but only periods that contain data are returned. If your organization started recently, fewer periods will be listed. Requesting periods outside this range returns 400.
GET /v1/mssp-report/{year}/{period} Reporting period detail with summary

Required Scope

mssp-report:read

Example 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"
  }
}
GET /v1/mssp-report/{year}/{period}/companies Eligible companies in a reporting period

Required Scope

mssp-report:read

Query Parameters

NameTypeDescription
searchstringFilter by company name or security_center_id
limitintegerPagination limit (default 100, max 1000)
offsetintegerPagination 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).

GET /v1/mssp-report/{year}/{period}/companies/{security_center_id}/usage Single company usage in a period

Required Scope

mssp-report:read

Query Parameters

NameTypeDescription
viewstringpeaks (default), daily, or all
productstringFilter by product code (e.g. SNS)
limitintegerPagination limit for daily rows (default 100, max 1000)
offsetintegerPagination 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.

Returns 403 if the company is not accessible, or 404 if the company was not active during this period.
GET /v1/mssp-report/{year}/{period}/usage/peaks Peak usage report for all companies

Required Scope

mssp-report:read

Query Parameters

NameTypeDescription
group_bystringproduct (default), company, or company,product
productstringFilter by product code (e.g. SNS)
limitintegerPagination limit (default 100, max 1000)
offsetintegerPagination 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.

Null handling: When a 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".
GET /v1/mssp-report/{year}/{period}/usage Full usage dump (peaks + daily per company)

Required Scope

mssp-report:read

Query Parameters

NameTypeDescription
productstringFilter by product code (e.g. SNS)
limitintegerCompanies per page (default 100, max 1000)
offsetintegerCompany-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

GET /v1/reseller-report List available calendar month periods

Required Scope

reseller-report:read

Period 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"
    }
  ]
}
Up to 6 calendar months are available (current + 5 previous), but only months with data are returned.
GET /v1/reseller-report/{year}/{period} Calendar month detail with summary

Required Scope

reseller-report:read

Response

{
  "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"
  }
}
GET /v1/reseller-report/{year}/{period}/companies Companies with active order status

Required Scope

reseller-report:read

Query Parameters

NameTypeDescription
searchstringFilter by company name or security_center_id
limitintegerPagination limit (default 100, max 1000)
offsetintegerPagination 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.

GET /v1/reseller-report/{year}/{period}/companies/{security_center_id}/usage Single company usage in a calendar month

Required Scope

reseller-report:read

Query Parameters

NameTypeDescription
viewstringpeaks (default), daily, or all
productstringFilter by product code (e.g. SNS)
limitintegerPagination limit for daily rows (default 100, max 1000)
offsetintegerPagination 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" }
  ]
}
GET /v1/reseller-report/{year}/{period}/usage/peaks Peak usage report for all active companies

Required Scope

reseller-report:read

Query Parameters

NameTypeDescription
group_bystringproduct (default), company, or company,product
productstringFilter by product code
limitintegerPagination limit (default 100, max 1000)
offsetintegerPagination 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 }
  ]
}
GET /v1/reseller-report/{year}/{period}/usage Full usage dump (peaks + daily per company)

Required Scope

reseller-report:read

Query Parameters

NameTypeDescription
productstringFilter by product code
limitintegerCompanies per page (default 100, max 1000)
offsetintegerCompany-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

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.

GET /v1/customers List active customers

Required Scope

customers:read

Query Parameters

NameTypeDescription
searchstringFilter by company name or security_center_id
limitintegerPagination limit (default 100, max 1000)
offsetintegerPagination 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"
    }
  ]
}
GET /v1/customers/{security_center_id} Customer detail with links

Required Scope

customers:read

Response

{
  "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"]
  }
}
GET /v1/customers/{security_center_id}/engagement Customer engagement metrics

Required Scope

customers:read

Query Parameters

NameTypeDescription
fromstringStart date YYYY-MM-DD (default: 30 days ago)
tostringEnd date YYYY-MM-DD (default: today)
typestringComma-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.

GET /v1/customers/{security_center_id}/churn Customer churn risk indicators

Required Scope

customers:read

Query Parameters

NameTypeDescription
fromstringStart date YYYY-MM-DD (default: 30 days ago)
tostringEnd date YYYY-MM-DD (default: today)
typestringComma-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

EndpointMethodRequired Scopes
/v1/auth/sessionPOSTNone (uses key pair)
/v1/auth/session/validatePOSTValid session
/v1/auth/sessionDELETEValid session
/v1/meGETme:read
/v1/mssp-reportGETmssp-report:read
/v1/mssp-report/{y}/{p}GETmssp-report:read
/v1/mssp-report/{y}/{p}/companiesGETmssp-report:read
/v1/mssp-report/{y}/{p}/companies/{id}/usageGETmssp-report:read
/v1/mssp-report/{y}/{p}/usage/peaksGETmssp-report:read
/v1/mssp-report/{y}/{p}/usageGETmssp-report:read
/v1/reseller-reportGETreseller-report:read
/v1/reseller-report/{y}/{p}GETreseller-report:read
/v1/reseller-report/{y}/{p}/companiesGETreseller-report:read
/v1/reseller-report/{y}/{p}/companies/{id}/usageGETreseller-report:read
/v1/reseller-report/{y}/{p}/usage/peaksGETreseller-report:read
/v1/reseller-report/{y}/{p}/usageGETreseller-report:read
/v1/customersGETcustomers:read
/v1/customers/{scid}GETcustomers:read
/v1/customers/{scid}/engagementGETcustomers:read
/v1/customers/{scid}/churnGETcustomers:read