ip database · api
Reporting API
Report abusive IPs and domains from your own tools: fail2ban, a WAF, a mail server, a log script. One report per request, or up to 1,000 at once. API reports are ordinary reports: same categories, same scoring, same moderation.
Keys
Create a key on your API keys page (you need an account with a confirmed email). It's shown once; we keep only a hash of it. Send it in a header, never in the URL:
Authorization: Bearer rws_live_…
Use one key per tool. Each report remembers the key it came with, so you can revoke a leaked key and withdraw what it sent without touching your other reports.
One report: POST /api/v1/reports
curl https://rowebsec.com/api/v1/reports \
-H "Authorization: Bearer $ROWEBSEC_KEY" \
-H "Content-Type: application/json" \
-d '{"target": "185.220.101.4", "categories": ["ssh"], "comment": "412 failed logins in 5 minutes"}'
| Field | Required | Notes |
|---|---|---|
| target | yes | One IPv4 or IPv6 address, or a domain. No ranges, no private addresses. |
| categories | yes | 1–10 category slugs, exactly as listed: ["ssh", "brute-force"] |
| observed_at | no | ISO 8601 with a time zone, like 2026-11-06T14:03:00Z. Defaults to now; at most 30 days back. |
| comment | no | Up to 1,000 characters. Public: no personal data. |
| evidence | no | Log lines, up to 4,000 characters. Longer is rejected, not cut. |
| domain | no | For an IP: the domain involved (phishing host, spam sender). |
| anonymous | no | true hides your name on the report. |
Answer, 201 Created:
{
"report": { "id": "0193…", "observed_at": "2026-11-06T14:03:00Z", "categories": ["ssh"], "anonymous": false },
"indicator": {
"target": "185.220.101.4", "kind": "ip", "score": 87, "verdict": "high", "reports": 41,
"score_pending": true, "whitelisted": false, "url": "https://rowebsec.com/ip/185.220.101.4"
}
}
The score and report count are from before your report: scores are recalculated a few seconds
later, once for a burst of reports, so a busy address doesn't slow your requests down.
score_pending says a recalculation is queued; the address's page shows the new score shortly.
Reports on whitelisted addresses (search engine crawlers, public DNS…) are accepted but don't score;
the answer says "whitelisted": true, "whitelisted_by": "Google", so your tool can stop reporting them.
Bulk: POST /api/v1/reports/bulk
Up to 1,000 reports and 2 MB per request, as JSON or CSV. Each item is the same as a single report, plus an optional ref (up to 64 characters) that comes back in the results.
curl https://rowebsec.com/api/v1/reports/bulk \
-H "Authorization: Bearer $ROWEBSEC_KEY" \
-H "Content-Type: application/json" \
-d '{"reports": [
{"ref": "a1", "target": "185.220.101.4", "categories": ["ssh"], "observed_at": "2026-11-06T14:03:00Z"},
{"ref": "a2", "target": "evil.example", "categories": ["phishing"], "comment": "Fake bank login"}
]}'
CSV with a header row (target required; categories, observed_at, comment optional; the ref is the row number):
curl https://rowebsec.com/api/v1/reports/bulk \
-H "Authorization: Bearer $ROWEBSEC_KEY" \
-H "Content-Type: text/csv" --data-binary @reports.csv
target,categories,observed_at,comment
185.220.101.4,"ssh,brute-force",2026-11-06T14:03:00Z,sshd brute force
evil.example,phishing,,Fake bank login
Valid items are saved even when others fail. Answer, 200 OK:
{
"submission_id": "0193…", "accepted": 1, "merged": 0, "duplicate": 0, "rejected": 1,
"results": [
{ "index": 0, "ref": "a1", "status": "accepted", "report_id": "0193…", "target": "185.220.101.4" },
{ "index": 1, "ref": "a2", "status": "rejected", "error": { "code": "invalid_target", "message": "…", "field": "target" } }
]
}
- accepted: saved as a report.
- merged: the same target within 15 minutes earlier in the batch; combined into that report (categories and comments joined). One line per failed login doesn't make 400 reports.
- duplicate: you reported this target in the last 15 minutes. Not an error; don't retry.
- rejected: with an
error, as below.
Scores and GeoIP for bulk reports are filled in a moment later. GET /api/v1/reports/bulk/:submission_id returns the same answer again for 7 days.
Errors
One shape everywhere:
{ "error": { "code": "invalid_target", "message": "10.0.0.5 is a private or reserved address…", "field": "target" } }
| HTTP | code | When |
|---|---|---|
| 400 | bad_request | The body isn't valid JSON or CSV, or is empty |
| 401 | unauthorized | Missing, unknown or revoked key, or a key in the URL |
| 403 | forbidden | The key lacks the reports:write scope, or the account is unconfirmed or banned |
| 409 | duplicate | You reported this target in the last 15 minutes |
| 413 | too_large | Over 1,000 items or 2 MB |
| 422 | invalid_target, invalid_category, invalid_field, self_report | The report breaks a rule; field says which |
| 429 | rate_limited, daily_limit | Too many requests or reports; wait for Retry-After seconds |
| 503 | unavailable | Reporting through the API is paused |
Limits
Per account, so more keys don't raise them. Web and API reports share the daily limit.
| First week | Confirmed | Trusted reporter | |
|---|---|---|---|
| Reports per 24 hours | 100 | 5,000 | 100,000 |
| Single-report requests per minute | 30 | 120 | 600 |
| Bulk requests per hour | 6 | 60 | 600 |
Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 carries Retry-After. Volume doesn't raise a score: one reporter counts once per address per day.
Retries
Send an Idempotency-Key header (any string up to 64 characters) to retry safely after a timeout:
the same key within 24 hours returns the first answer, with Idempotent-Replayed: true, instead of reporting again.
Categories
Use the slugs exactly as below. They never change. Also as JSON: GET /api/v1/categories (no key needed).
| Slug | Category | Family |
|---|---|---|
| brute-force | Brute-Force | Brute force |
| ftp-brute-force | FTP Brute-Force | Brute force |
| ssh | SSH Brute-Force | Brute force |
| bad-web-bot | Bad Web Bot | Web |
| sql-injection | SQL Injection | Web |
| web-app-attack | Web App Attack | Web |
| ddos | DDoS Attack | Network |
| iot-targeted | IoT Targeted | Network |
| port-scan | Port Scan | Network |
| spoofing | Spoofing | Network |
| email-spam | Email Spam | Spam |
| web-spam | Web Spam | Spam |
| fraud-orders | Fraud Orders | Fraud |
| fraud-voip | Fraud VoIP | Fraud |
| phishing | Phishing | Fraud |
| dns-compromise | DNS Compromise | Infrastructure |
| dns-poisoning | DNS Poisoning | Infrastructure |
| exploited-host | Exploited Host | Infrastructure |
| open-proxy | Open Proxy informational, doesn't score | Anonymizer |
| vpn | VPN IP informational, doesn't score | Anonymizer |
| hacking | Hacking | Other |
fail2ban
Save as /etc/fail2ban/action.d/rowebsec.conf:
[Definition]
actionban = curl -fsS --max-time 10 https://rowebsec.com/api/v1/reports \
-H "Authorization: Bearer <key>" -H "Content-Type: application/json" \
-d '{"target": "<ip>", "categories": ["<category>"], "comment": "fail2ban <name>: banned after <failures> failures"}'
[Init]
key =
category = brute-force
Then add it to a jail in jail.local, with the category that fits the jail (sshd → ssh, postfix → email-spam, nginx-botsearch → web-app-attack):
[sshd]
enabled = true
action = %(action_)s
rowebsec[key="rws_live_…", category="ssh"]