Developer Documentation

BadBot Gateway API

Use BadBot Gateway to look up IP and domain reputation, submit abuse reports, consume high-risk feeds, and correlate suspicious indicators.

Base URL
https://gateway.badbot.net

Quickstart

Start with a health check, create or generate an API key, submit a report, then query reputation for the same indicator.

1. Health check

curl https://gateway.badbot.net/api/v1/health
# → {"status":"ok","time":"2026-05-12T10:00:00+00:00"}

2. Get an API key

Sign in and generate a personal key under My API Keys. Copy it immediately; it is shown only once.

export BADBOT_API_KEY="paste-your-key-here"

3. Submit an abuse report

curl -X POST https://gateway.badbot.net/api/v1/report \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $BADBOT_API_KEY" \
  -d '{
    "indicator": "198.51.100.23",
    "category_id": 1,
    "severity": 6,
    "comment": "Credential stuffing"
  }'
# → {"status":"reported","id":42}

4. Query reputation

curl https://gateway.badbot.net/api/v1/reputation/198.51.100.23

Authentication

Write endpoints require the X-Api-Key request header.

Authenticated users can generate and revoke personal API keys from /api-keys. User API keys authorize report submission just like other API keys.

Admin-token endpoints use X-Admin-Token and are intended for operators, not normal client integrations.

Read-only endpoints (health, reputation, search, suggestions, feeds and correlation) never need an API key. The same per-IP rate limits apply with or without one.

Endpoint Overview

Method Path Auth Description
GET/api/v1/reputation/{indicator}—Look up score, reports, categories, and last report time.
GET/api/v1/search—Search indicators with pagination and country filter.
GET/api/v1/suggest—Autocomplete prefix search for indicators.
POST/api/v1/reportAPI keySubmit one abuse report.
POST/api/v1/report/bulkAPI keySubmit many reports in one request.
GET/api/v1/feeds/high-risk—Fetch high-risk indicators as JSON, CSV, or TXT.
GET/api/v1/correlation/{indicator}—Find related IPs through ASN, subnet, passive DNS and shared honeypot evidence.
POST /api/v1/report API key required

Submit Report

Submit a single abuse report for an IP address or domain.

Request body

FieldTypeRequiredDescription
indicatorstringyesIP address or domain name (max 253 chars).
category_idintegeryesAbuse category ID. See categories.
severityintegerno1–10. Higher values increase the reputation score. When omitted, the category's default severity is used.
commentstringnoFree-text note (max 500 chars).

Example request

curl -X POST https://gateway.badbot.net/api/v1/report \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $BADBOT_API_KEY" \
  -d '{
    "indicator": "198.51.100.23",
    "category_id": 1,
    "severity": 6,
    "comment": "Credential stuffing"
  }'

Response 201

{
  "status": "reported",
  "id": 42
}
POST /api/v1/report/bulk API key required

Bulk Report

Submit up to 1 000 reports in one request. The request body can be a JSON array or an object with a reports key. Each item uses the same fields as a single report. Invalid rows are collected in the errors array without blocking valid ones.

Example request

curl -X POST https://gateway.badbot.net/api/v1/report/bulk \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $BADBOT_API_KEY" \
  -d '[
    {"indicator":"198.51.100.23","category_id":1,"severity":6},
    {"indicator":"evil.example.com","category_id":9,"severity":8}
  ]'

Response 201

{
  "created": 2,
  "errors": [],
  "total": 2
}

Partial failure

{
  "created": 1,
  "errors": [
    {"index": 1, "error": "Category 99 does not exist"}
  ],
  "total": 2
}
GET /api/v1/reputation/{indicator}

Reputation Lookup

Returns the reputation score, report count, category breakdown, and last report timestamp for an IP address or domain. Scores decay over time using a configurable half-life.

Example request

curl https://gateway.badbot.net/api/v1/reputation/198.51.100.23

Response 200

{
  "indicator": "198.51.100.23",
  "found": true,
  "score": 42.5,
  "total_reports": 3,
  "categories": {
    "Credential Stuffing": 3
  },
  "last_reported_at": "2026-01-01T12:00:00+00:00",
  "allowlisted": false
}

When the indicator is unknown, found is false and score is 0.

GET /api/v1/suggest

Autocomplete Suggestions

Lightweight prefix search returning indicator values and types. Useful for search-as-you-type UIs. When q is omitted, returns the most recently reported indicators.

Query parameters

ParamTypeDefaultDescription
qstring—Prefix to match (optional, max 100 chars).
limitinteger10Max results (1–50).

Example request

curl "https://gateway.badbot.net/api/v1/suggest?q=198.51&limit=3"

Response 200

[
  {"value": "198.51.100.23", "type": "ip"},
  {"value": "198.51.100.44", "type": "ip"},
  {"value": "198.51.200.1", "type": "ip"}
]
GET /api/v1/correlation/{indicator}

Correlation

Find IP indicators related through shared ASN or subnet, passive DNS, and shared honeypot evidence (submitted credentials, client fingerprint, user agent). Currently supports IP addresses only.

Example request

curl https://gateway.badbot.net/api/v1/correlation/198.51.100.23

Response 200 (abbreviated)

{
  "seed": {
    "indicator": "198.51.100.23",
    "type": "ip",
    "found": true,
    "score": 42.5,
    "total_reports": 3,
    "meta": {
      "asn": "64496",
      "organization": "Example ISP",
      "country": "US"
    }
  },
  "matches": [...],
  "infrastructure": {
    "meta": {...},
    "domains": [...]
  },
  "spread_summary": {...},
  "activity_timeline": [...]
}

The response includes correlated matches, passive DNS infrastructure, activity timelines, and regional spread data.

Feeds

Feeds are available through stable web aliases and the API route. Use TXT for simple blocklists, CSV for imports, and JSON for structured automation.

Stable web aliases

/feeds/high-riskJSON feed (default limit: 1 000)
/feeds/high-risk.csvCSV — columns: indicator, score, reports, country_iso, asn
/feeds/high-risk.txtPlain text — one indicator per line
/feeds/high-risk-full-ipv4.txtFull IPv4 snapshot (all qualifying IPs, no limit cap)
/feeds/high-risk-full-ipv6.txtFull IPv6 snapshot (all qualifying IPs, no limit cap)

API route query parameters

ParamTypeDefaultDescription
min_scorefloat5Minimum reputation score to include.
include_verified_crawlerstrue|falsefalseAlso count category 16, Verified AI Crawler: shared vendor egress, left out so a blocklist built from this feed never blocks a vendor unless you choose to.
limitinteger1 000Max results (1–10 000).
typestring—ipv4, ipv6, or domain.
formatstringjsonjson, csv, or txt.

Example request

curl "https://gateway.badbot.net/api/v1/feeds/high-risk?type=ipv4&min_score=10&limit=100"

JSON response item

{
  "indicator": "198.51.100.23",
  "score": 42.5,
  "reports": 3,
  "country_iso": "US",
  "asn": "64496"
}
Truncation: When results exceed the limit, the response includes X-Truncated: true and X-Truncated-Limit response headers. The full snapshot feeds (-full-ipv4.txt, -full-ipv6.txt) are not subject to the limit cap.

Abuse Categories

Every report requires a category_id. The default severity is used when you omit the severity field.

ID Category Default Severity
1Credential Stuffing3
2SMTP Abuse4
3Web App Attack6
4DDoS8
5Spam5
6Botnet Drone7
7Malware9
8Scanning2
9Phishing8
10Fraud7
11Web Probe3
12Config Exposure Probe6
13Web Stack Probe5
14Admin Panel Probe5
15API Probe5
16Verified AI Crawler2

Rate Limits

Every API and feed endpoint is rate-limited per client IP, with or without an API key. When a limit is exceeded the response is 429 Too Many Requests with a Retry-After header giving the seconds to wait.

Endpoint Limit
GET /api/v1/health30 / min
GET /api/v1/reputation/{indicator}120 / min
GET /api/v1/search60 / min
GET /api/v1/suggest120 / min
POST /api/v1/report30 / min
POST /api/v1/report/bulk10 / min
GET /api/v1/feeds/high-risk10 / min
GET /feeds/high-risk, /feeds/high-risk.csv, /feeds/high-risk.txt10 / min each
GET /feeds/high-risk-full-ipv4.txt, /feeds/high-risk-full-ipv6.txt30 / min each
GET /api/v1/correlation/{indicator}30 / min

Errors

Error responses use a consistent JSON shape. The HTTP status code is mirrored in the body.

Error response format

{
  "error": "Bad Request",
  "message": "Category 99 does not exist",
  "status": 400
}

Status codes

  • 200 success (reads).
  • 201 success (writes).
  • 400 invalid input or payload shape.
  • 401 missing API key.
  • 403 invalid API key or token.
  • 404 resource not found.
  • 409 duplicate or conflicting resource.
  • 422 refused by policy: the address is on the gateway's allowlist and is not reported.
  • 429 rate limit exceeded — see Retry-After header.

Troubleshooting

  • 403 Invalid API key: regenerate the key if it was copied wrong or the API key salt changed.
  • 400 Category N does not exist: use one of the category IDs listed above.
  • Slow first report: enrichment may be performing RDAP, DNSBL, GeoIP, or WHOIS lookups in the background.
  • Feed format errors: use format=json|csv|txt on the API route or the stable web aliases under Feeds.

Code Examples

Python requests

import os
import requests

base_url = "https://gateway.badbot.net"
api_key = os.environ["BADBOT_API_KEY"]

payload = {
    "indicator": "198.51.100.23",
    "category_id": 1,
    "severity": 6,
    "comment": "Credential stuffing",
}

response = requests.post(
    f"{base_url}/api/v1/report",
    json=payload,
    headers={"X-Api-Key": api_key},
    timeout=10,
)
response.raise_for_status()

reputation = requests.get(
    f"{base_url}/api/v1/reputation/{payload['indicator']}",
    timeout=10,
).json()
print(reputation)

JavaScript fetch

const baseUrl = "https://gateway.badbot.net";
const apiKey = process.env.BADBOT_API_KEY;

const report = {
  indicator: "198.51.100.23",
  category_id: 1,
  severity: 6,
  comment: "Credential stuffing",
};

const response = await fetch(`${baseUrl}/api/v1/report`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Api-Key": apiKey,
  },
  body: JSON.stringify(report),
});

if (!response.ok) {
  throw new Error(`BadBot API error ${response.status}`);
}

PHP curl

<?php
$baseUrl = 'https://gateway.badbot.net';
$apiKey = getenv('BADBOT_API_KEY');
$payload = json_encode([
    'indicator' => '198.51.100.23',
    'category_id' => 1,
    'severity' => 6,
    'comment' => 'Credential stuffing',
]);

$ch = curl_init($baseUrl . '/api/v1/report');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'X-Api-Key: ' . $apiKey,
    ],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_RETURNTRANSFER => true,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status >= 400) {
    throw new RuntimeException('BadBot API error ' . $status . ': ' . $body);
}

Go net/http

package main

import (
	"bytes"
	"encoding/json"
	"net/http"
	"os"
)

func main() {
	payload, _ := json.Marshal(map[string]any{
		"indicator":   "198.51.100.23",
		"category_id": 1,
		"severity":    6,
		"comment":     "Credential stuffing",
	})

	req, _ := http.NewRequest("POST", "https://gateway.badbot.net/api/v1/report", bytes.NewReader(payload))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Api-Key", os.Getenv("BADBOT_API_KEY"))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		panic(resp.Status)
	}
}

Machine-Readable Docs

Use these endpoints for generated clients, API discovery, and agent-friendly context.

/openapi.json OpenAPI 3.0.3 service description
/.well-known/api-catalog API catalog linkset
/llms.txt LLM-friendly site and API index

Terms of Use

  • The API and feeds are free to use, including commercially, to protect your own networks, users and customers.
  • Attribution (“Data: BadBot.net”) is appreciated, not required.
  • Reselling the data, or redistributing it as (part of) a feed, dataset or product of your own, needs written permission from info [at] badbot.net.
  • The data is provided as is, without warranty. A listing records abuse our honeypot sensors observed; test before you block.
  • Listings expire on their own. To have one reviewed sooner, see delisting.