- REST API v1 with API-key auth, RFC 9457 errors and RateLimit headers.
- One-call mode:
GET /lookup/{lookupId}?wait=1runs every passive check server-side. - Origin tracks over the API: find real origin IPs behind CDNs.
- MCP server at /mcp (Streamable HTTP) with 8 tools for AI agents.
- OpenAPI 3.1 description at /openapi.json.
API reference · v1 · Professional
The Muswa APIfor scripts, SOAR and agents.
Everything the dashboard does, over HTTPS and MCP: investigate IPs, domains, networks, wallets and malware hashes, unmask origin servers hidden behind CDNs, and build shared investigation cases. Same engine, same quota, JSON in and out.
- Endpoints
- 8
- REST v1
- MCP tools
- 8
- Streamable HTTP
- Check ids
- 103
- 6 modules
- Requests / min
- 120
- per API key
On this pageIntroduction
Introduction
- 1
Get a key
Professional plan, then Dashboard → API. Export it as
MUSWA_KEY. - 2
Start a lookup
POST /lookup/{module}with a target. One lookup is charged; you get alookupIdand the check list. - 3
Collect results
GET /lookup/{lookupId}?wait=1runs every passive check server-side, or fetch/lookup/{lookupId}/{check}one by one. Both are free.
Format
JSON in, JSON out. UTF-8. Times are Unix milliseconds.
Errors
RFC 9457 problem+json with a stable machine code.
Versioning
Additive changes ship in v1. Breaking ones get /v2.
export MUSWA_KEY="mwk_live_…" # 1. start a lookup (charges 1 lookup) curl -X POST https://muswa.com/api/v1/lookup/domain \ -H "Authorization: Bearer $MUSWA_KEY" \ -H "Content-Type: application/json" \ -d '{"target":"suspicious-login.example"}' # 2. run every passive check in one call curl "https://muswa.com/api/v1/lookup/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d?wait=1" \ -H "Authorization: Bearer $MUSWA_KEY" --max-time 120
Authentication
Requirements
| Field | Type | Description |
|---|---|---|
| AuthorizationreqBearer mwk_live_…header | Bearer mwk_live_… | Key created in Dashboard → API. |
| PlanreqProfessional | Professional | API and MCP are part of Professional. Other plans get 402 api_required. |
| Content-Typeapplication/jsonheader | application/json | Required on POST requests with a body. |
Authorization: Bearer mwk_live_…
401 missing or revoked key
{
"type": "https://muswa.com/problems/unauthorized",
"title": "Invalid or revoked API key",
"status": 401,
"ok": false,
"error": "Invalid or revoked API key",
"code": "unauthorized"
}Rate limits & quotas
250lookups / day
Professional quota, shared with the dashboard. Resets 00:00 UTC.
5concurrent origin tracks
A slot frees itself when a track finishes, fails or times out.
| Scope | Limit | Keyed by |
|---|---|---|
| All /api/v1 endpoints | 120 | API key |
| POST /lookup/{module} | 30 | API key |
| GET /lookup/{lookupId}/{check} | 600 | API key |
| POST /origin | 10 | API key |
| MCP /mcp | 120 | API key |
| Everything under /api | 300 | client IP |
RateLimit (remaining r, seconds to reset t) and RateLimit-Policy. On 429 or 503, wait for Retry-After seconds before retrying.HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 RateLimit-Policy: "api-v1";q=120;w=60 RateLimit: "api-v1";r=117;t=42 HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json Retry-After: 18
Errors
application/problem+json (RFC 9457). Branch on code; show title to people. A failing check is not an HTTP error: it answers 200 with ok: false.| Status | Code | When | Retry |
|---|---|---|---|
| 400 | invalid_target | The target is not a valid IP, domain, ASN or wallet for this module. Nothing is charged. | no |
| 400 | bad_request | Unknown check id in checks, malformed parameters. | no |
| 400 | bad_json | The request body is not valid JSON. | no |
| 401 | unauthorized | Missing, malformed, invalid or revoked API key. Sent with WWW-Authenticate: Bearer. | no |
| 402 | api_required | The key owner has no active Professional plan. | no |
| 402 | plan_required | No active plan at all. | no |
| 404 | not_found | Unknown module, endpoint, lookup, check or track (or it belongs to someone else). | no |
| 409 | seek_limit | Every concurrent origin-track slot is busy. Wait for one to finish. | no |
| 410 | lookup_expired | The lookup is older than 24 hours. Start a new one. | no |
| 429 | rate_limited | Per-key or per-IP request rate exceeded. Honour Retry-After. | yes |
| 429 | quota_exceeded | Daily lookup quota reached. Retry-After points to 00:00 UTC. | yes |
| 500 | internal | Something broke on our side. Safe to retry with backoff. | yes |
| 503 | unavailable | Origin scanning is temporarily unavailable. Nothing was charged. | yes |
429 application/problem+json
- type"https://muswa.com/problems/quota_exceeded"
- title"Daily limit of 250 lookups reached. It resets at 00:00 UTC."
- status429
- okfalse
- error"Daily limit of 250 lookups reached. It resets at 00:00 UTC."
- code"quota_exceeded"
Account and quota
/api/v1/meResponse · 200
| Field | Type | Description |
|---|---|---|
| okreqtrue | true | Always true on 2xx. |
| userreqobject | object | Key owner. |
| idreqstring | string | User id. |
| usernamereqstring | string | Display name. |
| emailreqstring | string | Sign-in email. |
| apiKeyIdreqstring | string | Id of the key used (not the secret). |
| planreqobject | null | object | null | Active plan. |
| idreq"basic" | "professional" | "basic" | "professional" | Plan id. |
| endsAtinteger | integer | Unix ms when the paid period ends. |
| quotareqobject | object | Lookups today. |
| usedreqinteger | integer | Used today (UTC). |
| limitreqinteger | integer | Daily cap. |
| resetsAtreqinteger | integer | Unix ms of the next 00:00 UTC. |
| originreqobject | object | Origin-track slots. |
| runningreqinteger | integer | Tracks running now. |
| limitreqinteger | integer | Concurrent tracks allowed. |
curl "https://muswa.com/api/v1/me" \ -H "Authorization: Bearer $MUSWA_KEY"
{
"ok": true,
"user": {
"id": "p-b-DtpqUg4McPphIiE85A",
"username": "abuse-desk",
"email": "[email protected]"
},
"apiKeyId": "key_mvlf2gkteac05c4975",
"plan": {
"id": "professional",
"name": "Professional",
"endsAt": 1824317850771
},
"quota": {
"used": 25,
"limit": 250,
"resetsAt": 1792800000000
},
"origin": {
"running": 1,
"limit": 5
}
}Modules and checks
checks filter or to render results in the same order as the dashboard./api/v1/modulesResponse · 200
| Field | Type | Description |
|---|---|---|
| okreqtrue | true | Always true on 2xx. |
| modules[]reqModule[] | Module[] | One entry per module. |
| idreq"ip" | "domain" | "asn" | "aml" | "ip" | "domain" | "asn" | "aml" | Module id for POST /lookup/{module}. |
| labelreqstring | string | Display name. |
| inputreqstring | string | What the target looks like. |
| examplereqstring | string | A sample target. |
| checks[]reqCheckDef[] | CheckDef[] | Checks of this module. |
| idreqstring | string | Check id, used in URLs and in checks filters. |
| titlereqstring | string | Human title of the block. |
| weightreqinteger | integer | Importance 0-100. Higher renders first in the dashboard. |
| activeboolean | boolean | Touches the target (probe). Only runs when explicitly requested. |
| slowboolean | boolean | Usually takes longer than 10 s. |
curl "https://muswa.com/api/v1/modules" \ -H "Authorization: Bearer $MUSWA_KEY"
{
"ok": true,
"modules": [
{
"id": "ip",
"label": "IP Recon",
"input": "IPv4 or IPv6 address",
"example": "185.199.108.153",
"checks": [
{
"id": "geo",
"title": "Location",
"weight": 100,
"active": false,
"slow": false
},
{
"id": "abuse",
"title": "Abuse contact",
"weight": 96,
"active": false,
"slow": false
},
{
"id": "trace",
"title": "Traceroute",
"weight": 40,
"active": true,
"slow": true
}
]
}
]
}Start a lookup
/api/v1/lookup/{module}- A lookup stays usable for 24 hours: running or re-running its checks is free.
- Then either fetch each check in parallel, or call
GET /lookup/{lookupId}?wait=1once.
Path parameters
| Field | Type | Description |
|---|---|---|
| modulereq"ip" | "domain" | "asn" | "aml"path | "ip" | "domain" | "asn" | "aml" | Which recon module to run. |
Request body · application/json
| Field | Type | Description |
|---|---|---|
| targetreqstring | string | IP address, domain / hostname / URL, ASN (AS13335) or wallet address. Max 253 chars. Defanged input like evil[.]com is accepted. |
Response · 201
| Field | Type | Description |
|---|---|---|
| okreqtrue | true | Always true on 2xx. |
| lookupIdreqstring | string | UUID v4 lookup id for the next calls. |
| modulereqstring | string | Module id. |
| targetreqstring | string | Normalised target (lower-cased, punycode, refanged). |
| targetTypestring | string | ipv4 · ipv6 · domain · subdomain · asn · evm · btc · tron · sol |
| checks[]reqCheckDef[] | CheckDef[] | Checks available for this target, by weight. |
| idreqstring | string | Check id, used in URLs and in checks filters. |
| titlereqstring | string | Human title of the block. |
| weightreqinteger | integer | Importance 0-100. Higher renders first in the dashboard. |
| activeboolean | boolean | Touches the target (probe). Only runs when explicitly requested. |
| slowboolean | boolean | Usually takes longer than 10 s. |
| quotareqobject | object | Daily lookup usage after this call. |
| usedreqinteger | integer | Lookups used today (UTC). |
| limitreqinteger | integer | Daily limit of the plan. |
curl -X POST "https://muswa.com/api/v1/lookup/ip" \ -H "Authorization: Bearer $MUSWA_KEY" \ -H "Content-Type: application/json" \ -d '{"target":"185.199.108.153"}'
{
"ok": true,
"lookupId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"module": "ip",
"target": "185.199.108.153",
"targetType": "ipv4",
"checks": [
{
"id": "geo",
"title": "Location",
"weight": 100
},
{
"id": "privacy",
"title": "Privacy & classification",
"weight": 99
},
{
"id": "abuse",
"title": "Abuse contact",
"weight": 96
}
],
"quota": {
"used": 26,
"limit": 250
}
}Read a lookup (one-call mode)
wait, returns whatever checks already ran. With wait=1, runs every passive check that has no result yet on our side, with bounded concurrency, and returns all of them in a single response./api/v1/lookup/{lookupId}- The one-call mode answers within about 90 seconds. Checks still running at the deadline are listed in
pending: fetch them individually a little later. - Active checks (probes that touch the target) only run when you list them in
checks. - Set a client timeout of at least 100 s for
wait=1.
Path parameters
| Field | Type | Description |
|---|---|---|
| lookupIdreqstringpath | string | UUID v4 id from POST /lookup/{module}. |
Query parameters
| Field | Type | Description |
|---|---|---|
| wait"1"query | "1" | Run missing checks before answering. |
| checksstringquery | string | Comma-separated subset of check ids, e.g. geo,abuse,ptr. |
Response · 200
| Field | Type | Description |
|---|---|---|
| okreqtrue | true | Always true on 2xx. |
| lookupId / module / targetreqstring | string | Echo of the lookup. |
| createdAtreqinteger | integer | Unix ms. |
| completeboolean | boolean | wait=1 only: every requested check finished. |
| pendingstring[] | string[] | wait=1 only: check ids still running at the deadline. |
| resultsreq{ [checkId]: CheckResult } | { [checkId]: CheckResult } | One entry per finished check. |
| okreqboolean | boolean | false when the check failed. The lookup itself is still fine. |
| checkreqstring | string | Check id. |
| dataany | any | Check-specific JSON. Stable per check id within v1. |
| errorstring | string | Readable failure reason, present when ok is false. |
| emptyboolean | boolean | Ran fine, nothing found. |
| msreqinteger | integer | Run time in milliseconds. |
| cachedboolean | boolean | Served from our cache. |
curl "https://muswa.com/api/v1/lookup/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d?wait=1" \ -H "Authorization: Bearer $MUSWA_KEY" \ --max-time 120
{
"ok": true,
"lookupId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"module": "ip",
"target": "185.199.108.153",
"createdAt": 1792786962215,
"complete": true,
"pending": [],
"results": {
"geo": {
"ok": true,
"check": "geo",
"data": {
"city": "San Francisco",
"countryCode": "US",
"asn": 54113,
"asName": "Fastly, Inc."
},
"empty": false,
"ms": 184
},
"abuse": {
"ok": true,
"check": "abuse",
"data": {
"primary": {
"name": "Abuse desk",
"email": "[email protected]"
}
},
"empty": false,
"ms": 611
},
"ptr": {
"ok": true,
"check": "ptr",
"empty": true,
"ms": 42
}
}
}Run one check
ok: false: HTTP errors are reserved for bad requests./api/v1/lookup/{lookupId}/{check}Path parameters
| Field | Type | Description |
|---|---|---|
| lookupIdreqstringpath | string | UUID v4 lookup id. |
| checkreqstringpath | string | Check id from the lookup's checks list. |
Query parameters
| Field | Type | Description |
|---|---|---|
| *stringquery | string | Some interactive checks accept extra parameters (max 300 chars each). |
Response · 200
| Field | Type | Description |
|---|---|---|
| okreqboolean | boolean | false when the check failed. The lookup itself is still fine. |
| checkreqstring | string | Check id. |
| dataany | any | Check-specific JSON. Stable per check id within v1. |
| errorstring | string | Readable failure reason, present when ok is false. |
| emptyboolean | boolean | Ran fine, nothing found. |
| msreqinteger | integer | Run time in milliseconds. |
| cachedboolean | boolean | Served from our cache. |
curl "https://muswa.com/api/v1/lookup/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d/geo" \ -H "Authorization: Bearer $MUSWA_KEY"
{
"ok": true,
"check": "geo",
"data": {
"ip": "185.199.108.153",
"city": "San Francisco",
"region": "California",
"countryCode": "US",
"lat": 37.7749,
"lon": -122.4194,
"asn": 54113,
"asName": "Fastly, Inc.",
"anycast": true
},
"empty": false,
"ms": 184,
"cached": false
}Start an origin track
/api/v1/origin- Charges one lookup and holds one concurrent-track slot until it finishes (Professional: 5).
- A running track, or one that finished less than 1 hour ago, for the same domain is returned instead (
reused: true, 200) and costs nothing. Sendfresh: trueto force a new one. - If the scanner is unavailable, or the track never leaves the queue, the lookup is refunded.
Request body · application/json
| Field | Type | Description |
|---|---|---|
| domainreqstring | string | Domain, hostname or URL. |
| freshboolean | boolean | Ignore a recent track for the same domain. |
Response · 201
| Field | Type | Description |
|---|---|---|
| okreqtrue | true | Always true on 2xx. |
| seekIdreqstring | string | trk_… id to poll. |
| domainreqstring | string | Normalised domain. |
| reusedreqboolean | boolean | An existing track was returned (not charged). |
| quotareqobject | object | Daily lookup usage after this call. |
| usedreqinteger | integer | Lookups used today (UTC). |
| limitreqinteger | integer | Daily limit of the plan. |
curl -X POST "https://muswa.com/api/v1/origin" \ -H "Authorization: Bearer $MUSWA_KEY" \ -H "Content-Type: application/json" \ -d '{"domain":"example.com"}'
{
"ok": true,
"seekId": "trk_mvlet9id616efa2dd3",
"domain": "example.com",
"reused": false,
"quota": {
"used": 27,
"limit": 250
}
}Origin track results
wait./api/v1/origin/{seekId}phasegoes queued → scanning → done (or failed / timeout).statuscollapses it to running / done / failed.
Path parameters
| Field | Type | Description |
|---|---|---|
| seekIdreqstringpath | string | trk_… id. |
Query parameters
| Field | Type | Description |
|---|---|---|
| waitinteger 0-25query | integer 0-25 | Seconds to wait for the track to finish before answering. |
Response · 200
| Field | Type | Description |
|---|---|---|
| id / domainreqstring | string | Track id and domain. |
| statusreq"running" | "done" | "failed" | "running" | "done" | "failed" | Coarse state. |
| phasereqenum | enum | queued · scanning · done · failed · timeout |
| progressnumber 0-1 | number 0-1 | Probe progress while scanning. |
| total / matched / originsreqinteger | integer | Candidates, probe matches, and likely origins (non-CDN, confidence ≥ 50). |
| rows[]reqOriginRow[] | OriginRow[] | Up to 400 candidates. |
| ipreqstring | string | Candidate address. |
| portinteger | integer | Port where the evidence was seen. |
| confidencereqinteger | integer | 1-99 likelihood that this is the real origin. |
| matchedreqboolean | boolean | Answered as the domain during the active probe. |
| cdnreqstring | null | string | null | CDN that owns the IP. Set means edge node, not origin. |
| asn / asName / countryinteger / string / string | integer / string / string | Network owner and ISO country. |
| httpStatus / title / certCninteger / string / string | integer / string / string | What the host served during the probe. |
| evidence[]reqEvidence[] | Evidence[] | Why this IP is a candidate. |
| kindreqenum | enum | host_header · sni · default_cert · certificate · http · banner · ptr · other |
| detailreqstring | string | Readable explanation. |
| hoststring | string | Name that leaked (can be a subdomain). |
| firstSeen / lastSeeninteger | integer | Unix ms, from our daily internet-wide scans. |
| errorstring | string | Why a track failed or timed out. |
curl "https://muswa.com/api/v1/origin/trk_mvlet9id616efa2dd3?wait=20" \ -H "Authorization: Bearer $MUSWA_KEY" \ --max-time 120
{
"ok": true,
"id": "trk_mvlet9id616efa2dd3",
"domain": "example.com",
"status": "done",
"phase": "done",
"progress": 1,
"total": 2,
"matched": 1,
"origins": 1,
"rows": [
{
"ip": "203.0.113.24",
"port": 443,
"confidence": 96,
"matched": true,
"cdn": null,
"asn": 16276,
"country": "FR",
"httpStatus": 200,
"certCn": "example.com",
"evidence": [
{
"kind": "host_header",
"detail": "Answered with the site for a Host header probe",
"host": "example.com"
},
{
"kind": "certificate",
"detail": "Certificate SAN",
"host": "example.com",
"port": 443
}
]
},
{
"ip": "104.21.3.9",
"port": 443,
"confidence": 8,
"matched": true,
"cdn": "Cloudflare",
"asn": 13335,
"evidence": []
}
]
}List origin tracks
/api/v1/originQuery parameters
| Field | Type | Description |
|---|---|---|
| limitinteger 1-100query | integer 1-100 | How many tracks (default 30). |
Response · 200
| Field | Type | Description |
|---|---|---|
| seeks[]reqobject[] | object[] | Newest first. |
| id / domainreqstring | string | Track id and domain. |
| statusreq"running" | "done" | "failed" | "running" | "done" | "failed" | Coarse state. |
| matchedreqinteger | integer | Probe matches. |
| createdAt / finishedAtinteger | integer | Unix ms. |
| runningreqinteger | integer | Tracks running now. |
| limitreqinteger | integer | Concurrent tracks allowed. |
curl "https://muswa.com/api/v1/origin?limit=10" \ -H "Authorization: Bearer $MUSWA_KEY"
{
"ok": true,
"seeks": [
{
"id": "trk_mvlet9id616efa2dd3",
"domain": "example.com",
"status": "running",
"matched": 0,
"createdAt": 1792786532251,
"finishedAt": null
}
],
"running": 1,
"limit": 5
}Modules & check ids
GET /lookup/{lookupId}/{check}, in the checks filter, and in MCP tool calls. The live list is always at GET /modules. Active checks touch the target and only run when you ask for them.input: IPv4 or IPv6 address · e.g. 185.199.108.153
- 01
geoLocation - 02
localiseLatency geolocationactiveslow - 03
reputationReputation - 04
linkedDomains on this IP - 05
privacyPrivacy & classification - 06
abuseAbuse contact - 07
asnAutonomous system - 08
networkIP block - 09
exposureExposure - 10
blocklistsBlocklists - 11
routingRouting & RPKI - 12
companyCompany - 13
ptrReverse DNS - 14
confirmDomain confirmationactive - 15
portsPort & service scanactiveslow - 16
traceTracerouteactiveslow - 17
geotraceGeoTracerouteactiveslow - 18
openinOpen in
MCP server
https://muswa.com/mcp, Streamable HTTP, stateless, authenticated with your API key in the Authorization header. Tools that start a lookup charge it like the REST API.- 1
Create a key
Dashboard → API. Keys start with mwk_live_ and are shown once.
- 2
Register the server
Paste the command or JSON for your client, replacing the key.
- 3
Ask in plain words
"Who hosts 185.199.108.153 and where do I report abuse?"
| Tool | Arguments | What it does | Cost |
|---|---|---|---|
| ip_recon | target, checks? | Every passive IP check: location, owner, abuse contact, reputation, blocklists, exposure. | 1 lookup |
| domain_recon | target, checks? | DNS, registration, hosting and CDN, TLS, subdomains, phishing reputation, stack. | 1 lookup |
| asn_recon | target, checks? | Owner, prefixes, neighbours, abuse contact, fraud signals. | 1 lookup |
| aml_screen | target, checks? | Sanctions and illicit-exposure screening for a wallet. | 1 lookup |
| get_check | lookupId, check, params? | Full, untrimmed result of one check; runs active probes on demand. | Free |
| origin_find | domain, waitSeconds?, fresh? | Start an origin track and wait briefly for candidates. | 1 lookup |
| origin_status | seekId, waitSeconds?, limit? | Poll an origin track. | Free |
| account_usage | none | Plan, quota used today, running tracks. | Free |
get_check for the full result of one check, or for active probes.claude mcp add --transport http muswa https://muswa.com/mcp \ --header "Authorization: Bearer mwk_live_…"
curl https://muswa.com/mcp \ -H "Authorization: Bearer $MUSWA_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", "params":{"name":"origin_find","arguments":{"domain":"example.com"}}}'
Investigations & cases
The shape mirrors the rest of the API: JSON in and out, your key's plan and quota, results you can fetch without re-spending. A bulk check starts one lookup per indicator that still needs fresh data; re-reading a case never costs anything.
| Action | Shape | Cost |
|---|---|---|
| Create a case | POST /cases | Free |
| Add an indicator | POST /cases/{id}/items | Free |
| Bulk-check the case | POST /cases/{id}/check | 1 lookup / indicator |
| Read the timeline | GET /cases/{id}/history | Free |
| Export (PDF or ZIP) | GET /cases/{id}/export | Free |
Shapes above are illustrative; the authoritative request and response schemas live in openapi.json.
Changelog
Questions or a missing field? Write to [email protected]. Found a vulnerability? See the bug bounty.