Skip to content

API Overview ​

The ShadowMap API gives your own systems read-only access to your ShadowMap data — SIEM/SOAR pipelines, BI tools (Qlik, Power BI, Tableau), GRC platforms and custom automation. It serves the same scored data you see in the dashboard, as JSON or XML.

It exposes a connectivity check, a capabilities listing, the Vendor Risk Management export, your asset inventory and scan sessions, and the records of 17 modules: Alerts and IP Reputation, Web and Mobile Applications, six Data Leaks modules, five Dark Web modules, and SLA policies and violations. Each of those modules has a paged list of its records, a detail endpoint for one record and a summary of its counts. More modules are added over time; GET /api/v1/capabilities always lists the modules that exist, what each supports and whether your token can use it. The machine-readable contract is openapi.yaml (OpenAPI 3.0.3): it describes every field of every record.

Three properties define how the API behaves:

  • Read-only. There are no write or mutation endpoints.
  • Token authentication. Every request carries your bearer token, Authorization: Bearer <token>, over HTTPS. A client certificate is required only if your token was issued with one. IP allowlisting is optional, set by ShadowMap on request (see Authentication).
  • Scoped to your company. A token only ever returns your company's records, and there is no company_id parameter. One field reaches beyond your company by design: a Compromised Computers record's subdomains lists every sub-domain of any monitored organisation's credentials found on that computer, not only your own (see Data rules).

At a glance ​

TopicDetail
Base URLhttps://api.shadowmap.com/api/v1, or the hostname ShadowMap gives you at onboarding
Authenticationbearer token over HTTPS; a client certificate only if your token was issued with one; IP allowlisting optional
Token lifetime30 to 365 days, chosen when you create it
Limits60 requests per minute and 2 requests in flight, per token
Response formatJSON (the Vendor Risk Management export also offers XML)
Pagingpages of up to 500 records (200 on five heavy modules); follow next_cursor until it is null
TimesISO-8601 with an explicit offset (+05:30)
Change feedchanged_since on Compromised Users, Compromised Computers and SLA Violations only
Errors{"error": {"code", "message", "request_id"}} with a stable code
Request log retention365 days
Contractopenapi.yaml

Getting access ​

An Administrator of your ShadowMap account creates and manages API tokens in the dashboard. No request to ShadowMap is needed.

  1. Open Settings → Integrations → API Access.
  2. Choose Create token, give it a name, pick the scopes it needs (one per area of the dashboard, see Scopes) and a lifetime (30, 90, 180 or 365 days).
  3. Confirm with a one-time code: the code from your authenticator app if you use one, otherwise a code ShadowMap emails to you.
  4. Copy the token now. It is shown once and stored only as a hash, so a lost token cannot be recovered: revoke it and create another.

The same page lists every token of your account with its scopes, status, expiry and last use, and lets an Administrator revoke a token at once (also confirmed with a code).

  • Client certificate (optional). For teams that want certificate pinning, an Administrator can add a client certificate to a token with one click. The page downloads a package with the certificate and key as a .pfx file and as PEM files, and shows the passphrase once. Requests with that token to the API hostname then also need the certificate (see Client certificate). The private key is not kept by ShadowMap, so download the package once and store it safely. Removing the certificate returns the token to token-only access. A token that also works on your dashboard hostname (an integration set up before October 2026, marked Legacy host) cannot have a certificate added, because the certificate is checked only on the API hostname: create a new token to use a certificate. Tokens marked Managed by ShadowMap are shown for your information and cannot be changed on this page.
  • Expiry. A token is marked Expiring 30 days before it expires (a 30-day token: in its last 10 days). For a token you created, create a replacement and revoke the old one; for a token ShadowMap set up for you, the expiry emails ask you to contact ShadowMap to renew it.
  • Activity. The Activity tab shows every call made with your tokens: time, token, endpoint, status, error code and source address. You can filter by token, outcome and date.
  • Security emails. Every active Administrator of your account receives an email when a token is created or revoked, when a certificate is added or removed, and when a token is 30, 14 and 1 days from expiry (a token created with the 30-day lifetime skips the 30-day email).
  • IP allowlist. An allowlist is not set from the dashboard; it is optional and ShadowMap sets it on request through Contact Support (see IP allowlist).

If the API Access page is not shown for your account, or you prefer ShadowMap to set up an integration for you, raise a request through Contact Support with the integration's name, a technical contact and the scopes it needs.

Base URL and versioning ​

https://api.shadowmap.com/api/v1

The /v1 prefix versions the contract. Accounts hosted in a dedicated ShadowMap zone use that zone's own API hostname, which ShadowMap gives you at onboarding — the path structure is identical. Always call the hostname you were given: requests addressed to the API any other way are refused.

Authentication ​

The bearer token is the credential. Two further restrictions exist, and each applies only to a token that was set up with it: a client certificate, and an IP allowlist.

Bearer token ​

Send the full token on every request, over HTTPS:

bash
curl -H "Authorization: Bearer {token}" \
     https://api.shadowmap.com/api/v1/health
  • A token has the form {id}|smk_{secret}. Send the whole string, including the id and the smk_ part. The prefix lets secret scanners recognise a leaked ShadowMap token.
  • A token belongs to the service account ShadowMap created for your integration, and therefore to one company. It can never read another tenant's records.
  • A token expires at most 365 days after it was issued. GET /api/v1/health returns its expires_at, so your integration can watch its own credential.
  • Renewal. Administrators are emailed 30, 14 and 1 days before a token expires. Create a replacement in Settings → Integrations → API Access, switch your integration to it, then revoke the old token. For a token ShadowMap set up for you, ask ShadowMap for a successor: it has the same scopes, IP allowlist and client certificate requirement, and the old token keeps working for a 14-day overlap.
  • Revocation. An Administrator can revoke a token on the API Access page, or you can tell ShadowMap. The token stops working at once and returns 401 token_revoked.
  • Treat the token like a password: keep it out of source control, client-side code, URLs and chat. Since the token alone grants access unless you asked for one of the restrictions below, ask for only the scopes the integration needs.

Client certificate (optional) ​

A client certificate is required only if your token was issued with one. If you want one, generate the key and a certificate signing request (CSR) yourself and send ShadowMap only the CSR:

bash
openssl req -new -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj "/CN=my-integration"

ShadowMap signs it and returns client.crt, valid for one year, and issues your token tied to that certificate. Your private key never leaves your systems. Your client then presents the certificate during the TLS handshake on every request:

bash
curl --cert client.crt --key client.key \
     -H "Authorization: Bearer {token}" \
     https://api.shadowmap.com/api/v1/health
  • A token issued with a certificate is tied to that one certificate. A request that presents no certificate or a different one (even another one ShadowMap signed for you) returns 401 certificate_mismatch. A revoked certificate is refused at ShadowMap's network edge (403, an HTML page) before it reaches the API.
  • The certificate is valid for one year and is renewed together with the token: each yearly rotation of such a token comes with a new certificate, so send ShadowMap a new CSR.
  • A token issued without a certificate needs none, and any certificate your client happens to present is ignored.

Windows clients

Windows' built-in TLS stack (Schannel) is used by curl.exe on Windows and by .NET and PowerShell clients. It loads the client certificate from a PKCS#12 file, not from separate PEM files. Create one with openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12, then call curl.exe --cert client.p12:<password> --cert-type P12 .... In our testing, Schannel also failed the TLS 1.3 handshake with a certificate loaded from a file, and limiting the client to TLS 1.2 (--tls-max 1.2) solved it. OpenSSL-based clients, such as curl on Linux or macOS and Python requests, work over TLS 1.3.

IP allowlist (optional) ​

IP allowlisting is optional, set by ShadowMap on request. A token without an allowlist is accepted from any address. A token with one is accepted only from the IPv4 and IPv6 networks on its list, and from anywhere else it is refused with 403 ip_not_allowed.

  • An entry is a single address or a network: IPv4 from /8 to /32, IPv6 from /32 to /128, with the host bits zero (203.0.113.0/24 is valid, 203.0.113.5/24 is not).
  • IPv4 and IPv6 are separate entries. ShadowMap sees the address your connection actually used. If your network can reach the API over IPv6, a call may arrive from an IPv6 address even though you only listed IPv4, and it is then refused with 403 ip_not_allowed. List both families, or make your client use IPv4 only (curl -4).
  • An IPv4-mapped IPv6 address (::ffff:203.0.113.5) is not accepted: give the IPv4 form.
  • List your public egress address — the address your network presents to the internet — not an internal or NAT address.

Get your egress address right

If you call the API from a cloud function, container or VPN, the address your provider presents to the internet is what must be allowlisted. Verify it from the same network path before you send it to ShadowMap, and prefer a range if the address changes.

Scopes ​

A token carries one or more scopes, and each endpoint requires one. A token without the scope an endpoint needs gets 403 scope_missing. A module's list, detail and summary all need the module's scope.

ScopeGrants
alerts:readAlerts and IP Reputation
attack-surface:readWeb Applications and Mobile Applications
data-leaks:readS3 Buckets, Code Repositories, Leaked Credentials, Docker Containers, Leaked Files and Leaked APIs
dark-web:readData Breaches, Discussions, Compromised Users, Compromised Computers and Telegram
sla:readSLA policies and SLA violations
inventory:readyour asset inventory: domains, subdomains, IP addresses and their counts
scans:readyour scan sessions
vrm:readthe Vendor Risk Management export

/health and /capabilities need no scope. Ask for only the scopes the integration needs: a scope also grants the leaked values its modules hold, such as passwords and API credentials, in full (see Data rules). More scopes are introduced as more modules are released; GET /api/v1/capabilities shows, for each module, the scope it needs and whether your token holds it (granted).

Limits ​

LimitValueApplies to
Requests60 per minuteeach token
Requests in flight2 at the same timeeach token
  • Successful responses carry X-RateLimit-Limit and X-RateLimit-Remaining, and so does a rate_limited refusal (with X-RateLimit-Remaining: 0). A concurrency_limited refusal carries only Retry-After.
  • A 429 carries Retry-After, the number of seconds to wait. rate_limited means you used the minute's budget; concurrency_limited means more than 2 requests were running at once. A request refused as concurrency_limited still counts toward the minute's 60.
  • Limits are applied after your credentials are checked, so a request refused for a token, certificate or address reason never spends your budget.
  • A request that would start a database read more than 25 seconds after it reached the API is stopped and answered 503 api_unavailable (a read already under way can run for up to 30 seconds): retry it with a backoff, or narrow it (for example fewer status values).

Design your client to pull on a schedule, run requests one after another (or two at a time at most), and honour Retry-After instead of retrying in a tight loop.

Endpoints ​

MethodPathScopePurpose
GET/api/v1/healthnoneCheck that your token is accepted (with its certificate and source address, if your token has those restrictions).
GET/api/v1/capabilitiesnoneList the modules, what each supports, and whether your token can use each.
GET/api/v1/vendor-risk-management/vendorsvrm:readThe full Vendor Risk Management export.
GET/api/v1/inventory/domains, /subdomains, /ipsinventory:readYour asset inventory, one page at a time.
GET/api/v1/inventory/summaryinventory:readYour domains, subdomains and IP addresses counted, in total and by status.
GET/api/v1/scansscans:readYour scan sessions, one page at a time.
GET/api/v1/<path>the module'sOne page of a module's records (see Module records for each module's path).
GET/api/v1/<path>/{id}the module'sOne record.
GET/api/v1/<path>/summarythe module'sThe module's counts, by status tab and other dimensions (see Summaries).

Only GET (and HEAD) is supported. On the API hostname, any other method is refused at ShadowMap's network edge (403, an HTML page) before it reaches the API.

Health check ​

Use this to verify your token and network path before you wire up a real integration. (If your token was issued with a client certificate, add --cert client.crt --key client.key to this and every other example.)

bash
curl -H "Authorization: Bearer {token}" \
     https://api.shadowmap.com/api/v1/health
json
{
  "data": {
    "status": "ok",
    "time": "2026-10-09T09:30:00+05:30",
    "company_id": 12345,
    "token": {
      "id": 17,
      "name": "SIEM integration",
      "scopes": ["vrm:read"],
      "expires_at": "2027-10-09T09:30:00+05:30"
    }
  }
}

A 200 confirms that your token is accepted, together with its client certificate and source address when your token requires them. time and expires_at are ISO-8601 with an explicit offset (here +05:30). /health needs no scope, so it does not prove the token can call a particular endpoint: check token.scopes, or granted in /capabilities.

Capabilities ​

bash
curl -H "Authorization: Bearer {token}" \
     https://api.shadowmap.com/api/v1/capabilities

An abridged response (the real one lists every module):

json
{
  "data": {
    "api_version": "v1",
    "modules": [
      {
        "module": "vendor-risk-management.vendors",
        "scope": "vrm:read",
        "endpoints": ["GET /api/v1/vendor-risk-management/vendors"],
        "formats": ["json", "xml"],
        "filters": [],
        "discovery_column": null,
        "changed_since": false,
        "group_by": [],
        "pagination": null,
        "sort_keys": [],
        "refreshed": "hourly snapshot",
        "status_labels": [],
        "status_partition": null,
        "granted": true
      },
      {
        "module": "darkweb.compromised-users",
        "scope": "dark-web:read",
        "endpoints": [
          "GET /api/v1/darkweb/compromised-users",
          "GET /api/v1/darkweb/compromised-users/{id}",
          "GET /api/v1/darkweb/compromised-users/summary"
        ],
        "formats": ["json"],
        "filters": ["status", "discovered_from", "discovered_to", "changed_since"],
        "discovery_column": "seen_on",
        "changed_since": true,
        "group_by": ["status"],
        "pagination": { "key": "id", "default_page_size": 100, "max_page_size": 500 },
        "sort_keys": ["first_seen", "id"],
        "refreshed": "live",
        "status_labels": ["Needs Action", "Working Accounts", "Valid Users", "Action Taken", "To Be Closed", "Dismissed", "Mitigated"],
        "status_partition": true,
        "granted": false
      }
    ]
  }
}

Each module declares what it supports, so your client discovers it instead of guessing:

  • endpoints — the module's paths: for a module with records, its list, its detail and its summary.
  • filters — every filter parameter the module's list and summary accept, with the same values on both. The list also takes page_size, cursor and sort, the summary group_by (and top on Alerts and Web Applications); the detail takes no parameter.
  • status_labels — the dashboard's tab labels, as the status filter takes them and the summary reports them; for scans, the lifecycle states its lifecycle_state filter takes.
  • group_by — the dimensions the summary can count by. host, on Alerts and Web Applications, ranks the hosts with the most records (see Hosts at risk).
  • discovery_column — the record field discovered_from and discovered_to filter.
  • changed_since — whether the list and summary can return only the records changed since a moment (see Changes since your last pull).
  • pagination — how the list pages: key (the field records are ordered by), tiebreak (present when the key is not the id), default_page_size and max_page_size; null when the response is not paged.
  • sort_keys — the keys the list takes in sort (see Sorting); empty where the list takes no sort.
  • refreshed — live when the data is read at the time of your request, hourly snapshot for the Vendor Risk Management export.
  • status_partition — whether the module's status tabs are designed to cover every record (see Summaries); null for the Vendor Risk Management export, the inventory and the scans.
  • granted — whether your token holds the module's scope.

Vendor Risk Management export ​

Returns one row per vendor record for your company — the same dataset as the dashboard's Excel export, flattened for programmatic consumption.

bash
# JSON (default)
curl -H "Authorization: Bearer {token}" \
     https://api.shadowmap.com/api/v1/vendor-risk-management/vendors

# XML
curl -H "Authorization: Bearer {token}" \
     "https://api.shadowmap.com/api/v1/vendor-risk-management/vendors?format=xml"

The response is a flat array of vendor objects (not wrapped in a data envelope, and not paged), so it streams directly into BI table loaders. With format=xml it is an XML document with one <item> element per row. You can also ask for XML with an Accept: application/xml header when JSON is not also accepted; ?format= takes precedence.

An abridged row:

json
{
  "type": "current",
  "name": "Example Vendor Ltd",
  "initials": "EV",
  "scan_date": "2026-10-01 06:00:00",
  "score_date": "2026-10-01 07:00:00",
  "tags": "cloud,finance",
  "custom_tags": "tier-1",
  "overall_security_rating": "B",
  "overall_risk_score": 84,
  "alerts_rating": "B",
  "alerts_risk_score": 82,
  "alerts_high": 3,
  "alerts_medium": 12,
  "alerts_low": 8
}

Every row carries all of these field groups:

Field groupFieldsNotes
Identitytype, name, initials, scan_date, score_date, tags, custom_tagstype is current (latest assessment) or historical (an earlier assessment). scan_date is the authoritative scan start time, or null if the vendor has not been scanned yet. score_date is when the vendor's security rating was calculated — reported separately because score freshness can differ from scan freshness; null if the vendor has not been scored yet.
Overalloverall_security_rating, overall_risk_scoreLetter grade A–F plus the numeric 0–100 score.
Per-category ratingsalerts_*, brand_protection_*, data_leaks_*, dark_web_*, email_and_dns_*, encryption_and_certificates_*, application_security_*, vulnerability_management_*A _rating (letter) and a numeric score (_risk_score, or _score for the last four categories) for each of the eight scoring categories.
Finding counts{category}_high, {category}_medium, {category}_lowHigh/medium/low counts for the four finding-bearing categories (alerts, brand protection, data leaks, dark web).

Letter grades are derived from the numeric score on the standard band: A = 90–100, B = 80–89, C = 70–79, D = 60–69, F = below 60. This is the same banding used everywhere else in ShadowMap — see Security Rating Algorithm. Vendors assessed under the older four-category model return null for the four newer categories rather than omitting them, so the column set stays stable across your dataset.

Rows come in no guaranteed order

The export does not promise any row order. Use type and scan_date to order or de-duplicate rows in your own tooling.

No filters or sorting

The export has no filter or sort parameters; the only parameter is format. Filter and sort in your own tooling. (Earlier versions of this page listed priority, tag, score and sort parameters. They were never applied to the snapshot that normally answers the request, so they are not part of the contract.)

Snapshot-backed for consistency

For performance, the export is served from a snapshot that is refreshed hourly. A pull therefore sees a stable, point-in-time dataset rather than values that shift mid-scan, and pulling more often than hourly returns the same data. The same scoring data is browsable in the dashboard under Vendor Risk Management.

Inventory and scans ​

The inventory lists — GET /api/v1/inventory/domains, /inventory/subdomains and /inventory/ips — page through your asset inventory with the same page_size, cursor and next_cursor as module records (see Record lists), at most 500 per page. Domains and IP addresses are ordered by id and carry the values of the dashboard's Domains and IP Addresses exports; subdomains are one record per subdomain, with all its IP addresses, ordered by name. Each list takes one status value: In Scope or Out of Scope, subdomains also Not Reachable, IP addresses Online or Offline. GET /api/v1/inventory/summary counts each asset type in total and by status, and takes no parameter.

GET /api/v1/scans lists the scan sessions the dashboard's Reports page lists, in id order, and takes started_from and started_to (dates, +05:30) and lifecycle_state. A completed scan is sealed. Scans that ended before lifecycle tracking began (18 July 2026) have no lifecycle_state: treat a scan with no lifecycle_state and an ended_at as completed.

Filter scans with lifecycle_state; its accepted values are the status_labels that /capabilities lists for scans (sending status= returns 422 with the accepted names).

Module records ​

Seventeen modules serve records. Each has three endpoints under one path — the list GET /api/v1/<path>, the detail GET /api/v1/<path>/{id} and the summary GET /api/v1/<path>/summary — and all three need the module's scope.

ModulePath under /api/v1/ScopeLargest pageDiscovery columnModule filtersSort keys
Alertsthreats/alertsalerts:read500added_onseverity, finding_type, closed_byseverity, first_seen, last_seen, id
IP Reputationthreats/ip-reputationalerts:read500first_seenseverityfirst_seen, last_seen, id
Web Applicationsattack-surface/web-applicationsattack-surface:read200first_seen_onseverityseverity, first_seen, last_seen, id
Mobile Applicationsattack-surface/mobile-applicationsattack-surface:read500created_at—first_seen, id
S3 Bucketsdata-leaks/s3-bucketsdata-leaks:read500created_atseverityseverity, first_seen, id
Code Repositoriesdata-leaks/code-repositoriesdata-leaks:read200first_seen_onseverityseverity, first_seen, last_seen, id
Leaked Credentialsdata-leaks/leaked-credentialsdata-leaks:read500created_atimpact_levelfirst_seen, last_seen, id
Docker Containersdata-leaks/docker-containersdata-leaks:read500first_seen_onseverityseverity, first_seen, last_seen, id
Leaked Filesdata-leaks/leaked-filesdata-leaks:read500time_stampseverityseverity, first_seen, id
Leaked APIsdata-leaks/leaked-apisdata-leaks:read200leaked_onseverityseverity, first_seen, id
Data Breachesdarkweb/data-breachesdark-web:read200created_atseverityseverity, first_seen, last_seen, id
Discussionsdarkweb/discussionsdark-web:read500published_on—first_seen, id
Compromised Usersdarkweb/compromised-usersdark-web:read500seen_onchanged_sincefirst_seen, id
Compromised Computersdarkweb/compromised-computersdark-web:read200breach_datechanged_sincefirst_seen, id
Telegramdarkweb/telegramdark-web:read500messaged_at—first_seen, id
SLA Policiessla/policiessla:read500created_at—first_seen, id
SLA Violationssla/violationssla:read500created_atmodule, changed_sincefirst_seen, id

Every module's list and summary also take status, discovered_from and discovered_to. GET /api/v1/capabilities is the authority for all of this — each module's filters, the labels each module filter accepts (filter_labels), status_labels, group_by, discovery_column, changed_since, pagination and sort_keys — so have your client read them from there rather than hard-code them. openapi.yaml describes every field of every record, including how each module decides a record's status tab.

Record lists ​

GET /api/v1/<path> returns one page of the module's records for your company. An abridged page:

json
{
  "data": [
    { "id": 1203, "status": "Needs Review", "created_at": "2026-09-14T10:02:11+05:30" },
    { "id": 1304, "status": "Investigating", "created_at": "2026-09-15T08:45:00+05:30" }
  ],
  "meta": { "page_size": 2, "next_cursor": "djE6eyJrIjoxMzA0fQ" }
}

Paging:

  • page_size — 1 to the module's largest page (the table above, and max_page_size in /capabilities); 100 when you leave it out. Any other value is refused with 422, never silently clamped.
  • cursor — the next_cursor of the previous page, exactly as you received it; leave it out for the first page. It is opaque: do not build or edit it, and keep every other parameter the same while you walk. A cursor that is not in the API's format is refused with 422.
  • next_cursor — null on the last page. The walk ends when next_cursor is null, and only then. A page can hold fewer records than page_size and still have a next page — a record deleted or removed from your scope while its page is read is left out of that page — so never stop on a short page.
  • meta.page_size — the page size the request used, not the number of records on the page.
  • Order — without sort, ascending id in every module except Alerts, which is ascending added_on, then id (pagination.key and pagination.tiebreak in /capabilities); with sort, the order it asks for (see Sorting). Walking every page without sort (or with sort=id or sort=first_seen) returns each record once. Each page is read when you request it, so a record that changes during a walk is returned as it was when its page was read.

Filters:

  • Filters combine: a record is returned only when it matches every filter you give.
  • status — the dashboard's tabs, by their labels exactly as status_labels spells them: one label, or several separated by commas, for example status=Needs%20Review,Investigating. Several labels return the union of their tabs, each record once; a label given twice counts once. An unknown label or an empty entry is refused with 422. A record in no tab (its status is null) is never returned under a status filter.
  • discovered_from and discovered_to — when ShadowMap first found the record: they filter the module's discovery_column, so a window answers "what was found in September".
    • A date covers whole days at +05:30: discovered_from=2026-09-01 starts at 00:00 on 1 September, and discovered_to=2026-09-30 ends at 23:59:59 on 30 September.
    • A date-time is ISO-8601 with Z or an offset such as +05:30, for example 2026-09-01T09:30:00Z. An explicit offset is honoured; a date-time without one is read as +05:30. Send + as %2B: a bare + in a URL means a space, and the value is then refused.
    • A window that ends before it starts, and a date that does not exist, are refused with 422.
    • A record whose discovery time is unknown — none stored, or the zero date (0000-00-00 00:00:00) some older records hold — is in no window, not even one given only discovered_to. Its discovery field reads null; walk the list without a window to see it.
  • Module filters — the last column of the table, and in filters: severity, impact_level, finding_type, closed_by and module take the labels the module's summary groups by, in the same comma-separated form as status and with the same refusals, so the records listed under a label are the ones the summary counts under it. Alerts' closed_by returns Closed alerts only, by who closed them (analyst or scanner). IP Reputation's severity filter is refused with 422 for a company with more than 10,000 listings. changed_since has its own section.
  • Encode labels in the URL — a space as %20 and & as %26, for example module=Ransomware%20Group%20%26%20Forum on SLA Violations. No label contains a comma, so a comma always separates two labels.
  • Nothing is silently ignored. A parameter the endpoint does not take is refused with 422, and the message lists the ones it does take. So is a parameter given twice (status=Investigating&status=Closed — send one comma-separated value instead), in array form (status[]=Investigating) or with an empty value.

Sorting ​

sort orders a list by one of the module's sort keys (sort_keys in /capabilities, and the last column of the table above): sort=<key> ascending, sort=-<key> descending. The record id breaks ties, in the same direction.

Sorting is for ranking and top-N reads

severity and last_seen can change while you walk the pages: a record whose value moves during a walk can be skipped, or listed twice. For a complete pull, use no sort, sort=id or sort=first_seen (the discovery time does not change), and de-duplicate by id.

  • severity — by the risk behind the record's severity: sort=-severity puts the most severe first. On Alerts it is risk_score, the analyst's risk where one is set; on Code Repositories the numeric risk; elsewhere the severity label's own rank (for example Critical, High, Medium, Low on Data Breaches); a value outside the labels ranks lowest - last with -severity, first ascending.
  • first_seen — the module's discovery column, the field discovered_from and discovered_to filter.
  • last_seen — when a scan last saw the record, where the module records it.
  • id — the record id.
bash
# The five most severe open alerts
curl -H "Authorization: Bearer {token}" \
     "https://api.shadowmap.com/api/v1/threats/alerts?sort=-severity&page_size=5&status=Needs%20Review,Investigating,Accepted%20Risk"
  • One key. A second key (sort=severity,id) or a key the module does not offer is refused with 422, and the message lists the keys it takes. IP Reputation has no severity key, because its severity is computed per listing, and Leaked Credentials has none because its records carry impact_level instead.
  • A missing value — a record with no first_seen or last_seen — sorts before every value ascending and after every value descending.
  • Cursors follow their sort. A next_cursor continues only the sort it was returned under: send it back with the same sort. With another key, the other direction, or no sort at all it is refused with 422, and a cursor from a list without sort is refused by a sorted one. Without sort a list pages exactly as before, so cursors you already hold keep working.
  • Filters combine with sort: the filters choose the records, sort orders them.

Record details ​

GET /api/v1/<path>/{id} returns {"data": {...}}: the record the list returns for that id, with the same fields. It takes no query parameter, so any is refused with 422.

An id that is not one of your listed records answers 404 not_found, with the same body whichever it is: another company's record, one that does not exist, one the list leaves out (for example one removed from your scope), or an id not written as a plain integer (007). So the API cannot be used to learn whether another company's record exists. The dashboard still opens some records the API answers 404 for, such as one removed from your scope.

Summaries ​

GET /api/v1/<path>/summary counts the module's records: by the dashboard's status tabs, and by any other dimension the module declares in group_by.

bash
curl -H "Authorization: Bearer {token}" \
     "https://api.shadowmap.com/api/v1/threats/alerts/summary?discovered_from=2026-09-01&discovered_to=2026-09-30&group_by=status,severity"

An abridged response:

json
{
  "data": {
    "all": 1840,
    "removed_from_scope": 12,
    "groups": [
      { "status": "Needs Review", "severity": "High", "count": 41 },
      { "status": "Needs Review", "severity": "Medium", "count": 310 },
      { "status": "Investigating", "severity": "High", "count": 7 }
    ]
  },
  "meta": {
    "module": "threats.alerts",
    "group_by": ["status", "severity"],
    "status_partition": true,
    "generated_at": "2026-10-17T09:30:00+05:30"
  }
}
  • all — every record the module holds for you, whatever its tab. Records removed from your scope are not in it.
  • removed_from_scope — the records hidden because they left your scope (the dashboard's Removed From Scope tab); null for a module without that view. On Alerts it counts every descoped alert, including alerts all leaves out, so the two must not be added together.
  • groups — with no group_by, one count per status tab: every tab, zeros included, as the dashboard's badges show them. group_by takes up to three comma-separated dimensions from the module's group_by list, for example status,severity, or on Alerts custom_tag:<key> for one of your tag keys (one custom tag at a time). The order you give is the order of the keys and of the sort, and only non-zero combinations are returned. A record with several values for one custom tag is counted under each.
  • meta.status_partition — true when the module's tabs are designed to cover all, one tab per record. Even then a record left in a state no tab names is counted in all and in no tab. It is false for Discussions, Code Repositories and Leaked APIs, where some records sit in no tab by design.

Filters on the summary ​

The summary takes exactly the filters its list takes (filters in /capabilities), with the same labels and the same 422s, plus group_by (and top on Alerts and Web Applications); it refuses page_size, cursor and sort.

  • The discovery window, changed_since and the module filters narrow every count: each tab, all and removed_from_scope.
  • A status filter limits the status groups to the tabs it names — in tab order, zeros included — and all to the records in any of them. removed_from_scope is then null: a record removed from your scope is in no tab.
  • group_by then groups the records the filters leave.

So "September's alerts by status and severity" is one call, the example above, and "open SLA violations on alerts" is GET /api/v1/sla/violations/summary?status=Open,Escalated&module=Alerts, whose all is the number.

Hosts at risk ​

On Alerts and Web Applications, group_by=host ranks the hosts with the most records under the summary's filters: the top of them (1 to 100, default 10), by count descending and then host ascending. Each group is a host with its count and its by_severity counts — every severity label, zeros included. A host can be a domain or an IP address; only findings with no stored host (for example leaked credentials) are not ranked, so the hosts' counts add up to at most all. Hosts that differ only in letter case are one host. host is given on its own: it cannot be combined with another group_by dimension, and top without group_by=host is refused with 422.

bash
# The ten hosts with the most open alerts
curl -H "Authorization: Bearer {token}" \
     "https://api.shadowmap.com/api/v1/threats/alerts/summary?group_by=host&top=10&status=Needs%20Review,Investigating,Accepted%20Risk"
json
{
  "data": {
    "all": 5640,
    "removed_from_scope": null,
    "groups": [
      { "host": "portal.example.com", "count": 212, "by_severity": { "High": 9, "Medium": 61, "Low": 120, "Informational": 22 } },
      { "host": "www.example.com", "count": 187, "by_severity": { "High": 2, "Medium": 40, "Low": 101, "Informational": 44 } }
    ]
  },
  "meta": {
    "module": "threats.alerts",
    "group_by": ["host"],
    "status_partition": true,
    "generated_at": "2026-10-24T09:30:00+05:30",
    "top": 10
  }
}

Every filter narrows the ranking as it narrows the counts: add severity=High for the hosts with the most open High alerts, or a discovery window for the hosts most hit this month. Web Applications ranks the record's host - a domain or an IP address - each application counted once.

Totals reconcile with the list ​

Under the same filters, all is the number of records a full walk of the list returns, and each status group is the number a walk with status=<that tab> returns. Read the two close together: each is a separate read of live data, so a record that changes in between is counted by one and not the other. The exceptions are deliberate — in each, the summary counts as the dashboard's badges count:

  • Docker Containers counts an image once per details record ShadowMap holds for it — in all, in its tab and in every group — while the list returns the image once.
  • Code Repositories counts a repository with more than one details, index or extensions record once per record in every status tab except Takedown In Progress, while all, Takedown In Progress and removed_from_scope count it once, as the list returns it.
  • Docker Containers without a status filter (no filter, or only the discovery window and severity): the Requested Takedown, Taken Down and Filtered by AI groups also count an image that all leaves out — one that is not Dismissed and whose scanner status is neither online nor takendown. The list never returns such an image. Add a status filter and each tab counts only the images all holds.
  • removed_from_scope counts records the list never returns.

Changes since your last pull ​

Three modules can return only the records changed since a moment: Compromised Users, Compromised Computers and SLA Violations, the modules where every change to a record's fields moves its updated_at. /capabilities says changed_since: true for them and lists the parameter in their filters. Every other list and summary refuses changed_since with 422 rather than answer it incompletely: in those modules some changes leave every timestamp as it was, so do not use their updated_at to detect changes — pull the list whole, or compare summaries.

bash
curl -H "Authorization: Bearer {token}" \
     "https://api.shadowmap.com/api/v1/darkweb/compromised-users?changed_since=2026-10-16T06:00:00%2B05:30"
  • changed_since takes the forms discovered_from takes: a date is the start of that day at +05:30, and a date-time is ISO-8601 (send + as %2B).
  • It returns the records whose updated_at is at or after your moment, compared from 5 hours 30 minutes earlier on Compromised Users and Compromised Computers, and from the moment itself on SLA Violations. The stealer-log ingest that writes the two Dark Web modules stamps its changes with a UTC clock, so its stamps read 5 hours 30 minutes early; the allowance means none of them is missed, and it also returns ShadowMap's own changes from up to 5 hours 30 minutes before your moment. SLA violations have a single writer on ShadowMap's own clock, so they need no allowance.
  • It narrows the records, never their order — the list keeps its id order — and combines with every other filter. On the summary it narrows every count.

It over-reports, and it never misses a change to a field the record carries, if you overlap your pulls (see below). Every such change moves updated_at, so a pull from your moment returns every record changed since. It can also return records you already have: one changed shortly before your moment, one changed again, or — on the two Dark Web modules — one the ingest saw again without any visible change. Remove repeats by id.

What it cannot tell you:

  • Deletions. A deleted record is in no pull. ShadowMap deletes false-positive credentials outright, and deletes SLA violations with their policy, with a module type removed from their policy and with their finding, among other clean-ups.
  • Scope changes. A record that leaves your scope disappears from every list without a trace, and one restored to your scope comes back with its old updated_at, so no pull returns it.
  • Records without a change time. A record whose updated_at is unknown — none stored, or the zero date some older records hold — is in no pull until it changes.

Reconcile these with a periodic full pull, or by comparing the summary's all with your own count.

Overlap your pulls. A change is stamped when it is written, which can be a little before the API can see it. Start each pull from the moment your previous pull started, less a margin — we suggest an hour — and remove repeats by id.

Start from a full pull. Before this change feed was released, some changes did not move updated_at (an SLA violation's escalation, for one), so take one full pull first and use changed_since from then on.

Fields kept out of these records. None of the three records carries a field stored elsewhere whose changes leave the record's updated_at as it was: such a field could change without the record being returned. Read it where it lives:

RecordNot carriedRead it from
Compromised Usersthe computer's nameCompromised Computers, joined by hwid
Compromised Computersits credentials and its password countCompromised Users, joined by hwid, with that endpoint's own changed_since
SLA Violationsthe policy name and each level's due date/api/v1/sla/policies, by policy_id (see Violations)

A Compromised Computers record does carry your custom tags: every tag change moves the computer's updated_at.

SLA policies and violations ​

Policies ​

GET /api/v1/sla/policies lists every SLA policy of your company, whoever created it.

  • status is Active or Inactive, as the SLA Policies page labels the policy, and null for a stored status neither names. ShadowMap's SLA sweep — the recurring job that checks your findings against your policies and raises, escalates and closes violations — evaluates Active policies only.
  • escalation_enabled — when false, the policy's escalation levels are inert, and every violation it raises is created already Closed.
  • module_types lists the modules the policy covers: type (the SLA module type number, which its violations carry as module_type), label and evaluated. evaluated is false for every type the sweep does not evaluate, for which the policy raises no violation — for example IP Reputation and SSL Certificates, which can be chosen in the dashboard, or a type labelled Unknown.
  • escalations are the policy's levels, 1 to 4, in order; a level can be missing. A level is due offset days, or months (unit; how a month is counted is under Violations), after the start of the day the violation was raised (+05:30), and the sweep escalates an open violation on or after that day, one level at a time.
  • The list and summary take status, discovered_from and discovered_to (on created_at). There is no change feed: no field of the record tells you a policy was edited — editing a level does not move the policy's own time — so pull this list whole when you need it.
  • Not in the record: the policy's recipients and integrations, its creator and its matching criteria.

Violations ​

GET /api/v1/sla/violations lists every SLA violation of your company. ShadowMap's SLA sweep raises a violation when it first finds a finding that matches one of your Active policies, and closes it when the finding is settled or an analyst closes it; a finding that opens again gets a new violation. A record:

json
{
  "id": 88231,
  "web_url": "https://dashboard.example.com/dashboard/sla-violations/escalated",
  "status": "Escalated",
  "module_type": 2,
  "module": "Alerts",
  "finding_id": 5512094,
  "policy_id": 41,
  "created_at": "2026-10-02T11:00:04+05:30",
  "updated_at": "2026-10-09T00:30:12+05:30",
  "escalation_level": 2,
  "escalations": [
    { "level": 1, "escalated_at": "2026-10-05T00:30:09+05:30" },
    { "level": 2, "escalated_at": "2026-10-09T00:30:12+05:30" }
  ]
}
  • status is the dashboard's tab: Open (open and never escalated), Escalated (open and escalated at least once — the dashboard's tab, not a stored status) or Closed; null for a stored status no tab names. status=Open,Escalated returns every open violation. A policy that does not escalate creates its violations already Closed, so Closed does not always mean resolved.
  • module_type and module name the module of the violated finding: its SLA type number and its name (Unknown for a type ShadowMap has no name for). The module filter and group_by=module take those names, for example module=Alerts,Exposures.
  • finding_id is the finding's id in its module, so it joins to that module's records (the table below).
  • policy_id is the policy that raised the violation, in /api/v1/sla/policies.
  • created_at is the clock start: when the sweep first matched the finding, not when the finding was discovered. It is also the module's discovery column, so discovered_from and discovered_to filter it.
  • escalations is the violation's escalation history: each level it reached, once, in level order, with when it was first reached (escalated_at; null when no time was recorded). escalation_level is the highest level reached, null for a violation never escalated.
  • updated_at is when the violation last changed: set when it is raised, moved when it is closed, each time it is escalated to a level, and when it is re-pointed to another finding. changed_since follows it. An escalation recorded before this change feed was released did not move it, so begin with one full pull.
  • web_url opens the dashboard tab the violation is listed under; the dashboard has no page for one violation.
module_typemoduleRecords at
1Exposures/api/v1/attack-surface/web-applications
2Alerts/api/v1/threats/alerts
5Ransomware Group & Forum/api/v1/darkweb/discussions
6Data Breaches/api/v1/darkweb/data-breaches
7Docker Container/api/v1/data-leaks/docker-containers
12Leaked APIs/api/v1/data-leaks/leaked-apis
13Leaked File/api/v1/data-leaks/leaked-files
14Code Repositories/api/v1/data-leaks/code-repositories
17S3 Bucket/api/v1/data-leaks/s3-buckets
20Telegram Conversations/api/v1/darkweb/telegram
24Malware Compromised Users/api/v1/darkweb/compromised-users
25Malware Compromised Computers/api/v1/darkweb/compromised-computers
31Leaked Credentials/api/v1/data-leaks/leaked-credentials

Mobile Application (15) and IP Reputation (19) have record endpoints too, but the sweep does not evaluate them, so it raises no violation on them; the other module types have no endpoint in this version. Where a module lists one record for several stored copies — Web Applications and Data Breaches (see Data rules) — the finding can be listed there under another id, and its own id then answers 404.

Why a violation carries no policy name and no due dates. Both come from the policy, and editing the policy changes them without moving the violation's updated_at, so a changed_since pull would never show the change. Read them from /api/v1/sla/policies by policy_id, pulling that list whole, and compute each level's due date from the violation's created_at: the start of that day (+05:30) plus the level's offset in its unit. Count a month offset as the sweep does: a day the target month lacks rolls over into the next month, so one month after 31 January is 3 March (2 March in a leap year) — up to 3 days past the end of the shorter month. The number of days a violation has been open is not a field either: compute it from created_at.

Data rules ​

Rules every module's records follow; the contract describes every field.

  • Leaked values are returned in full. Passwords (password in Data Breaches and Compromised Users), leaked secrets (matching, before, after and probe_metadata in Leaked Credentials) and leaked requests (auth, headers, params and body in Leaked APIs) are returned exactly as stored — not masked, not truncated — even where the dashboard masks or hides them. Telegram messages are returned whole and can quote leaked material. Store and log these records as secrets, and give each token only the scopes its integration needs.
  • id identifies a record within its module. Use it to follow a record from one pull to the next, with two exceptions:
    • Web Applications. A record is one logical application: its proto, host, port and path, the host and path compared case-insensitively. ShadowMap can hold several copies of one application — a CDN copy, a copy found again at another IP address, a closed earlier copy — and represents it by one of them, the one the dashboard shows. The record's id is that copy's, so it can change, for example when a scan closes the representative while another copy is still live; the old id then answers 404. Follow an application by (proto, host, port, path).
    • Data Breaches. A credential stored more than once is one record, under the id of the copy kept for it: the copy linked to one of your tracked executives when there is one, otherwise the one stored first (the lowest id). That id is stable only while the rule keeps the same copy — adding or removing an executive can move the credential to another id — and the id of a copy that is not kept answers 404.
  • Executive names are your own executives' only. Data Breaches is the only module whose records name an executive, and it names your own tracked executives only: a breach linked to an executive who is not one of yours — another company's, or one no longer tracked — has is_executive: false and executive_name: null. Compromised Users and Compromised Computers only say whether a record is linked to one of your tracked executives (is_executive).
  • Times are ISO-8601 with an offset, +05:30 (for example 2026-10-01T08:30:00+05:30): compare them as instants, not as text. A time ShadowMap does not know — none stored, or the zero date some older records hold — is null. A value that is not an instant is returned as stored text and never read as a time, such as a breach's breach_date (2019-01, sometimes a bare year). Where a collector's clock differs from ShadowMap's, the module's contract says so: the stealer-log ingest stamps created_at and updated_at in UTC, so on Compromised Users and Compromised Computers a time it wrote reads 5 hours 30 minutes early.
  • status is a tab label. A record's status is the dashboard tab it is under, one of the module's status_labels, and null for a record in no tab. A stored status code is returned only where a module shows it beside the tab, for transparency.
  • A token sees the whole company. The integration's account is not narrowed by any dashboard user's organisation or saved-search restrictions, so its lists and counts can be larger than what a restricted dashboard user sees.
  • One field names other organisations' sub-domains. A Compromised Computers record's subdomains lists every sub-domain of any monitored organisation's credentials found on that computer — not only your own. Your own credentials for the computer are Compromised Users records: join them by hwid.
  • web_url opens the record on your dashboard.

A minimal client ​

python
import time
from datetime import datetime, timedelta, timezone

import requests

BASE = "https://api.shadowmap.com/api/v1"

session = requests.Session()
session.headers["Authorization"] = "Bearer {token}"   # the full token string
# session.cert = ("client.crt", "client.key")         # only if your token was issued with a client certificate

def get(path, params=None, retries=5):
    for _ in range(retries):
        response = session.get(BASE + path, params=params, timeout=90)
        if response.status_code == 429:               # rate_limited or concurrency_limited
            time.sleep(int(response.headers.get("Retry-After", "1")))
            continue
        response.raise_for_status()
        return response
    raise RuntimeError("still rate limited after %d attempts" % retries)

def walk(path, params=None):
    """Every record a module list returns under params, page after page."""
    params = dict(params or {})
    while True:
        page = get(path, params).json()
        yield from page["data"]
        if page["meta"]["next_cursor"] is None:       # the only end of a walk: a short page is not one
            return
        params["cursor"] = page["meta"]["next_cursor"]

vendors = get("/vendor-risk-management/vendors").json()

# September's alerts that still need review, and their counts by severity
september = {"status": "Needs Review", "discovered_from": "2026-09-01", "discovered_to": "2026-09-30"}
alerts = list(walk("/threats/alerts", dict(september, page_size=500)))
by_severity = get("/threats/alerts/summary", dict(september, group_by="severity")).json()["data"]["groups"]

# A monthly report: the five most severe open alerts, and the ten hosts with the most open alerts
open_alerts = {"status": "Needs Review,Investigating,Accepted Risk"}
top_alerts = get("/threats/alerts", dict(open_alerts, sort="-severity", page_size=5)).json()["data"]
top_hosts = get("/threats/alerts/summary", dict(open_alerts, group_by="host", top=10)).json()["data"]["groups"]

# After one full pull: compromised users changed since the previous pull started, less an hour of overlap
def changed_users(previous_start):
    started = datetime.now(timezone.utc)
    since = (previous_start - timedelta(hours=1)).isoformat(timespec="seconds")
    users = {user["id"]: user for user in walk("/darkweb/compromised-users", {"changed_since": since})}
    return users, started                              # keep `started` for the next pull

Responses and errors ​

Every response from the API, success or failure, carries an X-Request-Id header. Quote it when you contact support: it finds the exact request in ShadowMap's log.

Some requests never reach the API, because ShadowMap's network edge refuses them first:

  • a revoked client certificate;
  • a path outside /api/v1/;
  • a method other than GET or HEAD.

They get an HTML 403 page with no JSON error body and no X-Request-Id.

Every error has the same shape. Branch on the HTTP status first, then on error.code, which is stable; the message is for humans and may be reworded.

json
{
  "error": {
    "code": "ip_not_allowed",
    "message": "Your source IP address is not on this token's allowlist.",
    "request_id": "01J9ZQ8K3M4N5P6R7S8T9V0W1X"
  }
}
CodeStatusMeaning and what to do
origin_not_verified403The request did not reach the API through ShadowMap's network edge, for example because it was addressed to a server directly instead of the API hostname. Use the hostname you were given; if you do, send the request_id to support.
token_invalid401The bearer token is missing, malformed or unknown. Send the full token string from onboarding in the Authorization: Bearer header.
token_expired401The token is past its expiry. Switch to the successor issued at rotation, or ask ShadowMap for a new token.
token_revoked401The token was revoked. Ask your ShadowMap contact for a new one.
token_not_permitted403The token is not valid for the hostname or path you used — for example a token issued for the API hostname used on the dashboard hostname — or it was never set up for API use. Contact support.
account_disabled403The account behind the token is suspended, or access for your organisation is disabled. Contact support.
certificate_mismatch401Your token was issued with a client certificate, and the request presented none or a different one. Present the certificate issued for this token. A token issued at a yearly rotation is tied to the new certificate that came with it, not to the old one.
ip_not_allowed403Your token has an IP allowlist, and the address your request came from is not on it. Check both IPv4 and IPv6 (see IP allowlist) and ask support to add the missing range.
scope_missing403The token does not hold the scope this endpoint needs — for a module's list, detail or summary, the module's scope (see Scopes). granted in /capabilities shows which modules your token can read.
rate_limited429More than 60 requests in a minute. Wait Retry-After seconds.
concurrency_limited429More than 2 requests in flight. Wait Retry-After (1 second) and keep to 2 parallel requests.
not_found404No such path: check the URL and the /api/v1 prefix. On a record detail, the id is not one of your listed records — another company's, one that does not exist, one the list leaves out, or one not written as a plain integer — and every case gets the same response.
method_not_allowed405The method is not supported. The API is read-only: use GET. (On the API hostname, other methods are refused at the network edge before they reach the API.)
validation_failed422A query parameter is invalid or not accepted here; message names it and why. For example: a parameter the endpoint does not take (such as changed_since outside the three modules that offer it, or any parameter on a record detail); a parameter given twice, in array form or with no value; a page_size outside 1 to the module's largest page; a cursor that is not in the API's format; an unknown label or an empty entry in status or a module filter; a date that is malformed or does not exist, or a window that ends before it starts; a group_by the module does not offer; a sort key the list does not offer, or a cursor sent with another sort than the one it came from; a top outside 1 to 100, or without group_by=host; IP Reputation's severity filter for a company with more than 10,000 listings. Fix the request before retrying.
api_unavailable503The API is temporarily unavailable on ShadowMap's side, or the request ran out of time (see Limits). Retry with a backoff, and narrow a request that keeps running out of time; contact support if it persists.
server_error500An unexpected error. ShadowMap is notified automatically. Retry later, and quote the request_id if it persists.

Retry only 429 (after Retry-After), 500 and 503 (with a backoff). Every other 4xx needs something changed first.

Diagnosing a 401 or a 403

A 401 means ShadowMap could not accept your token, or the certificate your token requires (token_invalid, token_expired, token_revoked, certificate_mismatch). A 403 with a JSON body means your credentials were recognised but are not permitted here — for a token with an IP allowlist most often ip_not_allowed: check which family (IPv4 or IPv6) your network used, and that the address is on the token's allowlist. A 403 that is an HTML page came from ShadowMap's network edge: your client used a method or path the API does not serve, or presented a revoked client certificate.

Logging and monitoring ​

ShadowMap logs every request to the API, including refused ones, and keeps the log for 365 days, then deletes it. A log entry holds the time, the request id, your token and company, the source address, method, path and query string, the response status and error code, the response size and number of rows returned, your user agent and the fingerprint of the client certificate, if one was presented. Credential-like query parameters (such as a token, key, secret or password) are stored redacted — but never put a credential in a URL.

The log supports support requests and security monitoring. ShadowMap alerts its own operations team on patterns such as repeated refusals, a token used from a new address or a sudden jump in volume, and may contact your technical contact about them.

Integrations set up before October 2026 ​

An integration that was set up before October 2026 and has been in regular use calls the API on your dashboard hostname with its existing token and no client certificate. It keeps working there for now. It follows the same limits (60 requests per minute, 2 in flight), returns the error format above, and its token has an expiry date; on the dashboard hostname its health check keeps its earlier response shape.

To move it to the API hostname, change only its base URL to the one in Base URL and versioning: the same token works there, with the same scopes, and needs no client certificate. ShadowMap will tell you before the dashboard-hostname path is closed. An older token that has not been used recently is no longer accepted (token_not_permitted): ask ShadowMap for a new one.

A note on ingest endpoints ​

You may see an /api/ingest/* path referenced. These are internal, server-to-server endpoints used by ShadowMap's own collection pipelines and authenticate with a shared secret, not a bearer token. They are not part of the customer-facing API and are not available for external use.

Common questions ​

How do I get a token? An Administrator creates it in Settings → Integrations → API Access (see Getting access). For an IP allowlist, contact support with your egress addresses (IPv4 and IPv6).

Do I need a client certificate or an IP allowlist? No. The bearer token is enough. A client certificate is required only if your token was issued with one, and IP allowlisting is optional, set by ShadowMap on request. Either adds a second check: a leaked token is then useless without the certificate's private key, or from outside your networks.

I get 403 even though my token is valid. Why? Read error.code. For a token with an IP allowlist it is usually ip_not_allowed: the address your network presented is not on the token's allowlist, or your network used IPv6 and only IPv4 was listed. List your public egress addresses, both families.

I get 401 certificate_mismatch. Your token was issued with a client certificate, and the request did not present that certificate. Check that your client is loading the client.crt and client.key issued for this token. During a rotation overlap the old token works only with the old certificate and the new token only with the new one, so switch both together.

Can one token read data for several companies? No. A token is bound to a single company, and there is no company_id parameter. The one field that names other organisations' sub-domains is a compromised computer's subdomains (see Data rules).

Do tokens expire? Yes, at most 365 days after issue. GET /api/v1/health shows expires_at. Plan the renewal about a month ahead, and the 14-day overlap at rotation lets you switch without downtime. If your token was issued with a client certificate, that certificate also expires after one year: generate a new key and CSR and send the CSR with the renewal request, and the new token and the new certificate arrive together.

Is 60 requests per minute enough? For a scheduled pull, yes. The Vendor Risk Management export is one response, so it needs one request. A full pull of a module is one request per page: 100,000 records at 500 per page is 200 requests. Counts need no pages — ask the summary — and on Compromised Users, Compromised Computers and SLA Violations changed_since pulls only what changed. Fetch on a cadence instead of polling in a loop, and keep to 2 requests in flight.

Can I sort a list? Yes, by one key: sort=-severity, sort=first_seen, sort=-last_seen or sort=id where the module offers it (sort_keys in /capabilities; see Sorting). Keep the same sort for every page of a walk.

How do I know I have every page? When meta.next_cursor is null. A page shorter than page_size does not mean the walk is over (see Record lists).

Can I pull only what changed since my last sync? On Compromised Users, Compromised Computers and SLA Violations, yes: changed_since (see Changes since your last pull). The other modules have changes that move no timestamp, so the API refuses changed_since there rather than return an incomplete answer: pull those lists whole, or compare their summaries.

A record id I stored now answers 404. Why? The record is no longer one of your listed records: it was deleted, it left your scope, or — on Web Applications and Data Breaches — ShadowMap now lists it under another id (see Data rules). A full pull shows the current set.

Why does a summary count differ from the records I pulled? Read both close together and with the same filters. The differences that remain are deliberate and listed under Totals reconcile with the list.

Can I write or modify data through the API? No. The API is read-only.

What data is available today? A health check, a capabilities listing, the full Vendor Risk Management export (JSON or XML), your asset inventory and scan sessions, and the records, details and counts of 17 modules (see Module records). More modules are added over time; /capabilities lists what exists.

Does the API support XML? The Vendor Risk Management export does: append ?format=xml or send Accept: application/xml. Every other endpoint returns JSON.

A key or token may have been exposed. What do I do? Revoke the token on the API Access page at once, then create a replacement. If a client certificate key may be exposed too, revoking the token makes it useless; contact ShadowMap if you need help.

  • openapi.yaml — the OpenAPI 3.0.3 contract for this API.
  • Vendor Risk Management — the dashboard view of the same vendor scoring data the API exports.
  • SLA Policies and SLA Violations — how the policies the API lists match findings and escalate, and the violations they raise.
  • Custom Tags — your own tag keys and values, returned in custom_tags and counted by custom_tag:<key> on Alerts.
  • Security Rating Algorithm — how the numeric scores and A–F letter grades returned by the API are calculated.
  • Exports — manual JSON/CSV/Excel/PDF exports from within the dashboard, an alternative to the API for one-off pulls.
  • Integrations — outbound integrations (SIEM, ticketing, chat) that push ShadowMap findings to other systems.
  • Data Retention — how long the underlying data the API serves is retained.
  • Contact Support — request API access, token rotation and allowlist changes.

ShadowMap - External Attack Surface Management