DEV Community

IP99
IP99

Posted on

Import IP99's OpenAPI Once: Postman, Codegen, and GPT Actions

Most IP-risk vendors hand you a PDF and a support ticket. IP99 by ThreatHunter publishes an OpenAPI 3.1 document at a stable URL you can import the same way you import any other public API: Postman, Insomnia, openapi-generator, or a GPT Action. One file describes the free profile, the quota endpoint, and the professional Pulse path — so your clients, types, and agent tools stay in sync with the live service instead of a blog post you copied six months ago.

This is a practical import walkthrough. Every curl below runs without an API key. Live samples were captured on 2026-10-31 (UTC).

What the spec actually covers

curl -s https://ip99.com/openapi.json | head -c 200
# → {"openapi":"3.1.0","info":{"title":"IP99 IP risk and geolocation API",...
Enter fullscreen mode Exit fullscreen mode

As of today the document advertises OpenAPI 3.1.0, title IP99 IP risk and geolocation API, version 1.0.0, server https://ip99.com, and three paths:

Path What it is
GET /v1/ip/{ip} Free risk + geo + network profile (key optional)
GET /v1/me Your current tier and remaining quota
GET /pulse/v1/ip/{ip} Same score with per-evidence detail (professional key)

Access-Control-Allow-Origin: * is set on the OpenAPI JSON itself, so browser-based importers and GPT Action validators can fetch it without a CORS proxy. Security schemes in the file are exactly what the human docs say: X-API-Key header or Authorization: Bearer, never a query string (?key= is rejected with HTTP 400 key_in_query).

The free endpoint is still the one you should try first:

curl https://ip99.com/v1/ip/8.8.8.8
Enter fullscreen mode Exit fullscreen mode

Captured ~2026-10-31 13:05 UTC:

{
  "ip": "8.8.8.8",
  "computed_at": "2026-10-31T13:05:33Z",
  "evidence_state": "none",
  "geo": {
    "continent": "NA",
    "country": "US",
    "region": "California",
    "city": "Mountain View",
    "district": "",
    "tz": "America/Los_Angeles"
  },
  "network": { "asn": 15169, "usage_type": "IDC" },
  "risk": {
    "score": 0,
    "level": "none",
    "signals": ["hosting"]
  }
}
Enter fullscreen mode Exit fullscreen mode

hosting here is a network attribute, not a risk verdict. Score 0 with evidence_state: none means "no current verifiable evidence," not "safe." Official copy is blunt about that; your generated client should expose evidence_state as a first-class field, not bury it under "extra."

Import into Postman (or Insomnia)

  1. Copy https://ip99.com/openapi.json.
  2. Postman: Import → Link → paste the URL → Import. Insomnia: Create → Import From → URL.
  3. You get three requests. Open lookupIP (GET /v1/ip/{ip}), set ip to 8.8.8.8, send. No Authorization needed for anonymous quota.
  4. Optional: add a collection variable apiKey and map it to the apiKey / bearer scheme when you create a free key in the console. Fields do not change with a key — only the quota counter does.

Anonymous allowance today (from GET /v1/me): 500 lookups/day per client IP, 60/min, resetting at midnight Asia/Shanghai. Sign in and self-create a key for a higher daily bucket; overseas self-serve Pro / Pro+ buys volume, not a different score. Per-item evidence rows, share_tier, and ?at= history sit on Pulse and need a professional key.

Generate a typed client

Any OpenAPI 3.1 generator that understands 3.1 will work. Example with the official CLI (install once, then pin the URL):

npx @openapitools/openapi-generator-cli generate \
  -i https://ip99.com/openapi.json \
  -g typescript-fetch \
  -o ./ip99-client
Enter fullscreen mode Exit fullscreen mode

Or Python:

openapi-generator-cli generate \
  -i https://ip99.com/openapi.json \
  -g python \
  -o ./ip99-py
Enter fullscreen mode Exit fullscreen mode

What you should assert in a smoke test after generation:

  1. Response type includes evidence_state enum: active | stale | none.
  2. risk.score is documented (and treated in app code) as freshness/strength of evidence, not "probability this visitor is an attacker."
  3. risk.signals is an array of slugs; hosting / mobile describe the network and are not, by themselves, a ban reason.
  4. Errors are { "error": { "code", "message" } } — branch on error.code, never on the prose in message.

A second live call, using IP99's documented demo address (mask the last octet of real customer IPs in tickets):

curl https://ip99.com/v1/ip/27.22.66.200
Enter fullscreen mode Exit fullscreen mode

Same window:

{
  "ip": "27.22.66.200",
  "computed_at": "2026-10-31T13:05:57Z",
  "evidence_state": "active",
  "geo": {
    "country": "CN",
    "region": "Hubei",
    "city": "Xiangyang",
    "district": "Yicheng",
    "tz": "Asia/Shanghai"
  },
  "network": { "asn": 4134, "usage_type": "DYN" },
  "risk": {
    "score": 7,
    "level": "low",
    "signals": ["dialup_pool", "hijacked_proxy", "proxy"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Residential usage_type: DYN plus proxy-class signals is exactly why "ban all IDC ASN ranges" fails: the interesting exits often sit on home broadband. Official triage order: read evidence_state first, then signals, then score/level — and prefer step-up verification over hard-banning a whole building when the address may be shared.

Wire it as a GPT Action (or similar)

Custom GPTs / Actions that accept an OpenAPI URL can point at the same file. Practical checklist:

  • Schema URL: https://ip99.com/openapi.json (CORS already allows browser fetch).
  • Auth for free experiments: none. For production volume, header X-API-Key.
  • Restrict the Action to GET /v1/ip/{ip} (and maybe /v1/me) unless the agent truly has a professional Pulse key. Leaving Pulse in the Action without a key only produces 403 professional_required noise.
  • Prompt the model to report evidence_state and computed_at alongside the score. A stale or empty evidence state with score 0 must not be summarized as "clean IP."

If you want agents without a custom GPT, IP99 also exposes the same free profile as an MCP tool ip_risk_lookup at https://ip99.com/mcp — same fields as /v1/ip/{ip}. That path is covered in an earlier post; the OpenAPI file remains the right source of truth for HTTP clients and codegen.

Quotas, headers, and what not to invent

Every successful lookup can carry daily quota headers (X-Daily-Limit, X-Daily-Remaining, X-Daily-Reset). Prefer those (or GET /v1/me) over hard-coding numbers from an old article — allowances can change, and the response is authoritative for your tier.

Do not cache risk verdicts across sessions: the product thesis is that every answer is a judgement that expires. computed_at is part of what makes a decision auditable. Pulse's ?at= replay exists for chargeback-style "what did we know then?" questions; the free /v1/ip endpoint answers now only.

Also out of scope for this import guide (and for honest marketing): inventing customer logos, accuracy percentages, funding claims, or equating IP99's free profile with ThreatHunter's full enterprise datasets. IP99 is a ThreatHunter product; the free JSON you get from /v1/ip is exactly what the OpenAPI schema for that path describes — nothing more.

Try it

  1. Import https://ip99.com/openapi.json.
  2. Hit /v1/ip/8.8.8.8 with no key.
  3. Branch your app on evidence_state before you trust a zero.

Human reference: https://ip99.com/api. Product home: https://ip99.com.

Top comments (1)

Collapse
 
alexshev profile image
Alex Shev •

Treating the OpenAPI document as the shared contract is a strong way to remove integration drift. The next safeguard is contract testing in CI, so generated clients and action definitions fail together when a response shape changes.