A modern, production-ready Java client for the proxycheck.io v3 API.
Detect proxies, VPNs, TOR nodes, and disposable emails with confidence.
Installation • Quick Start • Documentation • Contributing • License
The proxycheck.io API is powerful but raw HTTP calls leave a lot of boilerplate on your plate: retries, caching, rate limiting, response parsing, error handling. This library wraps all of that into a clean, fluent API that a Java developer can pick up in minutes.
- Zero boilerplate — one line to create a client, one line to check an address.
- Production-hardened — exponential backoff retries, LRU cache, token-bucket rate limiter.
- Type-safe — sealed
Resulttypes, record-based models, exhaustiveswitchsupport. - Lightweight — single runtime dependency (Gson). No frameworks, no reflection magic.
| Feature | Description |
|---|---|
| Sync & Async API | Blocking calls and CompletableFuture-based non-blocking calls |
| Response Caching | LRU cache with configurable TTL and max size |
| Rate Limiting | Token-bucket limiter to stay within API quotas |
| Exponential Backoff | Automatic retry for transient network and server failures |
| Address Whitelist | Skip API calls entirely for trusted IPs |
| Event Listeners | Hook into request, response, error, cache hit, and retry events |
| Sealed Result Types | Exhaustive pattern matching with IpResult and EmailResult |
| Query Flag Presets | detailed(), minimal(), withNode() for common configs |
| Address Validation | Client-side IPv4, IPv6, and email format validation |
| Result Filtering | Built-in methods for threat IPs, safe IPs, disposable emails |
| Batch Processing | Check up to 1,000 addresses per request with automatic splitting |
| JPMS Support | Proper module-info.java for modular Java applications |
- Java 21 or later
- Gradle 9+ (included via wrapper) or Maven 3.8+
repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
dependencies {
implementation("com.github.SquareCodeFX:proxycheck-io-v3-api:21233954d3")
}dependencies {
implementation 'com.github.SquareCodeFX:proxycheck-io-v3-api:21233954d3'
}<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.SquareCodeFX</groupId>
<artifactId>proxycheck-io-v3-api</artifactId>
<version>21233954d3</version>
</dependency>git clone https://github.com/SquareCodeFX/proxycheck-io-v3-api.git
cd proxycheck-io-v3-api
./gradlew buildimportio.proxycheck.api.*;
importio.proxycheck.api.model.*;
// Create a client (uses sensible defaults: 30s timeout, 3 retries)try (varclient = ProxyCheckClient.of("your-api-key")) {
// Check a single IPvarresponse = client.check("8.8.8.8");
response.firstIpResult().ifPresent(ip -> {
System.out.println("Threat: " + ip.isThreat());
System.out.println("Risk: " + ip.riskLevel());
System.out.println("Country: " + ip.countryCode());
});
// Check an emailvaremailResponse = client.check("user@tempmail.org");
emailResponse.firstEmailResult().ifPresent(email ->
System.out.println("Disposable: " + email.isDisposable()));
}- Client Configuration
- Checking Addresses
- Query Flags
- Working with Responses
- IP Result Details
- Email Result Details
- Risk Assessment
- Caching
- Rate Limiting
- Retry Policy
- Whitelist
- Event Listeners
- Address Validation
- Error Handling
- Java Module System
Use ProxyCheckClient.of(key) for quick setup, or the builder for full control:
varclient = ProxyCheckClient.builder()
.apiKey("your-api-key")
.timeout(Duration.ofSeconds(15))
.cache(Duration.ofMinutes(5), 2000)
.rateLimitPerSecond(150)
.retryPolicy(RetryPolicy.builder()
.maxRetries(3)
.initialDelay(Duration.ofMillis(500))
.multiplier(2.0)
.maxDelay(Duration.ofSeconds(8))
.build())
.whitelist("127.0.0.1", "::1")
.listener(CheckListener.builder()
.onRequest(addrs -> log.info("Checking: {}", addrs))
.onResponse((addrs, resp) -> log.info("Status: {}", resp.status()))
.onError((addrs, err) -> log.error("Failed: {}", err.getMessage()))
.onCacheHit(addr -> log.debug("Cache hit: {}", addr))
.onRetry((attempt, cause) -> log.warn("Retry #{}", attempt))
.build())
.build();| Option | Default | Description |
|---|---|---|
apiKey(String) | required | Your proxycheck.io API key |
timeout(Duration) | 30s | HTTP request timeout |
httpClient(HttpClient) | built-in HTTP/2 | Custom java.net.http.HttpClient instance |
cache(Duration) | disabled | Enable LRU cache with TTL (default max: 1,000 entries) |
cache(Duration, int) | disabled | Enable LRU cache with TTL and custom max size |
rateLimitPerSecond(int) | disabled | Token-bucket rate limit (recommended: 150) |
retryPolicy(RetryPolicy) | 3 retries, exponential backoff | Custom retry behavior |
noRetry() | — | Disable all retry attempts |
whitelist(String...) | empty | Addresses that bypass the API entirely |
whitelist(Collection) | empty | Addresses that bypass the API entirely |
listener(CheckListener) | none | Lifecycle event listener |
// Default flagsvarresponse = client.check("8.8.8.8");
// With query flagsvarresponse = client.check("8.8.8.8", QueryFlags.detailed());Batch requests use HTTP POST and automatically split into groups of 1,000 when needed:
// From a collectionvarresponse = client.check(List.of(
"8.8.8.8", "1.1.1.1", "user@example.com"
));
// Varargs shorthandvarresponse = client.checkMultiple("8.8.8.8", "1.1.1.1");
// With flagsvarresponse = client.check(
List.of("8.8.8.8", "1.1.1.1"),
QueryFlags.create().days(7).node(true)
);All check methods have async variants returning CompletableFuture:
// Singleclient.checkAsync("8.8.8.8")
.thenAccept(response -> {
if (response.hasThreat()) {
System.out.println("Threat detected!");
}
});
// Batchclient.checkAsync(List.of("8.8.8.8", "1.1.1.1"))
.thenAccept(response -> response.threatIps().forEach(ip ->
System.out.println("Blocked: " + ip.address())));
// With flagsclient.checkAsync("8.8.8.8", QueryFlags.detailed())
.thenAccept(this::processResponse);Control the level of detail the API returns:
// Built-in presetsQueryFlags.detailed() // node info + 7-day historyQueryFlags.minimal() // short (flat) response formatQueryFlags.withNode() // include cluster node identifier// Custom combinationvarflags = QueryFlags.create()
.node(true)
.days(30)
.tag("login-flow")
.ver("11-February-2026");| Method | API Parameter | Description |
|---|---|---|
node(boolean) | node | Include responding cluster node |
shortResponse(boolean) | short | Flat response format |
prettyPrint(boolean) | p | Pretty-print JSON |
days(int) | days | Historical detection window |
tag(String) | tag | Dashboard tracking label |
noTag() | tag=0 | Disable query logging |
ver(String) | ver | Pin API version |
varresponse = client.check("8.8.8.8");
response.isOk(); // status == OKresponse.isWarning(); // status == WARNING (approaching limits)response.isDenied(); // status == DENIED (quota exceeded)response.isError(); // status == ERROR (invalid input)response.isSuccessful(); // OK or WARNING (results are usable)// Resolve to a known status message enumStatusMessagemsg = response.statusMessage();
if (msg == StatusMessage.NEAR_QUERY_LIMIT) {
log.warn("Approaching daily query limit");
}// By addressresponse.ipResult("8.8.8.8").ifPresent(ip -> ...);
response.emailResult("user@example.com").ifPresent(email -> ...);
// First result (convenient for single-address checks)response.firstIpResult().ifPresent(ip -> ...);
response.firstEmailResult().ifPresent(email -> ...);
// Unified lookup via the sealed Result interfaceresponse.result("8.8.8.8").ifPresent(result -> {
switch (result) {
caseIpResultip -> handleIp(ip);
caseEmailResultemail -> handleEmail(email);
}
});// Built-in filtersList<IpResult> threats = response.threatIps();
List<IpResult> safe = response.safeIps();
List<EmailResult> trash = response.disposableEmails();
List<EmailResult> legit = response.legitimateEmails();
// Boolean checksbooleanhasThreat = response.hasThreat();
booleanhasDisposable = response.hasDisposableEmail();
// Custom predicateList<IpResult> highRisk = response.ipResultsMatching(
ip -> ip.riskLevel() == RiskLevel.VERY_HIGH);
List<IpResult> fromUS = response.ipResultsMatching(
ip -> "US".equals(ip.countryCode()));// Stream all results with pattern matchingresponse.streamResults().forEach(result -> {
switch (result) {
caseIpResultip -> processIp(ip);
caseEmailResultemail -> processEmail(email);
}
});
// Stream only IPsresponse.streamIpResults()
.filter(IpResult::isThreat)
.forEach(ip -> blockAddress(ip.address()));
// Stream only emailsresponse.streamEmailResults()
.filter(EmailResult::isDisposable)
.map(EmailResult::address)
.forEach(this::rejectRegistration);Each IpResult aggregates all data sections the API may return:
varip = response.firstIpResult().orElseThrow();
// ── Convenience Accessors ──────────────────────────────────────ip.address(); // "8.8.8.8"ip.isThreat(); // true if proxy, VPN, TOR, compromised, or scraperip.riskLevel(); // LOW | MEDIUM | HIGH | VERY_HIGHip.accessRecommendation(); // ALLOW | CHALLENGE | DENYip.countryCode(); // "US"ip.provider(); // "Google LLC"ip.networkType(); // RESIDENTIAL | BUSINESS | WIRELESS | HOSTING// ── Network ────────────────────────────────────────────────────ip.network().asn(); // "AS15169"ip.network().range(); // "8.8.8.0/24"ip.network().hostname(); // "dns.google"ip.network().provider(); // "Google LLC"ip.network().organisation(); // "Google LLC"ip.network().type(); // "Business"// ── Location ───────────────────────────────────────────────────ip.location().continentName(); // "North America"ip.location().countryName(); // "United States"ip.location().countryCode(); // "US"ip.location().regionName(); // "California"ip.location().cityName(); // "Mountain View"ip.location().latitude(); // "37.386"ip.location().longitude(); // "-122.084"ip.location().timezone(); // "America/Los_Angeles"ip.location().currency(); // Currency[code=USD, name=Dollar, symbol=$]// ── Detections ─────────────────────────────────────────────────ip.detections().isProxy(); // falseip.detections().isVpn(); // falseip.detections().isTor(); // falseip.detections().isCompromised(); // falseip.detections().isScraper(); // falseip.detections().isHosting(); // trueip.detections().isAnonymous(); // falseip.detections().isThreat(); // aggregated: proxy|vpn|tor|compromised|scraperip.detections().risk(); // 0-100ip.detections().confidence(); // 0-100ip.detections().firstSeen(); // ISO 8601 timestamp or nullip.detections().lastSeen(); // ISO 8601 timestamp or null// ── Device Estimate ────────────────────────────────────────────ip.deviceEstimate().address(); // devices behind this IPip.deviceEstimate().subnet(); // devices in the subnet// ── Detection History ──────────────────────────────────────────ip.detectionHistory().delisted(); // true = already delistedip.detectionHistory().delistDatetime(); // ISO 8601 timestamp// ── Attack History ─────────────────────────────────────────────ip.attackHistory().attacks(); // Map<String, Integer>ip.attackHistory().totalAttacks(); // sum of all attack counts// ── Operator (VPN/proxy provider info) ─────────────────────────ip.operator().name(); // "Cloudflare WARP"ip.operator().url(); // "https://..."ip.operator().anonymity(); // "Low"ip.operator().popularity(); // "Very High"ip.operator().services(); // ["VPN"]ip.operator().protocols(); // ["WireGuard"]ip.operator().policies(); // OperatorPolicies recordvaremail = response.firstEmailResult().orElseThrow();
email.address(); // "user@tempmail.org"email.isDisposable(); // true| Level | Score | Interpretation |
|---|---|---|
LOW | 0 – 25 | Minimal risk, typically safe |
MEDIUM | 26 – 50 | Moderate risk, warrants monitoring |
HIGH | 51 – 75 | Elevated risk, likely suspicious |
VERY_HIGH | 76 – 100 | Severe risk, strongly associated with abuse |
RiskLevellevel = RiskLevel.fromScore(66); // HIGHCombines the risk score with the anonymity flag to produce an access decision:
| Risk Level | Anonymous | Not Anonymous |
|---|---|---|
| LOW | CHALLENGE | ALLOW |
| MEDIUM | CHALLENGE | CHALLENGE |
| HIGH | DENY | CHALLENGE |
| VERY_HIGH | DENY | DENY |
AccessRecommendationrec = ip.accessRecommendation();
// Or evaluate manuallyAccessRecommendation.evaluate(66, true); // DENYAccessRecommendation.evaluate(10, false); // ALLOWThe built-in LRU cache stores successful responses keyed by address + query flags:
varclient = ProxyCheckClient.builder()
.apiKey("key")
.cache(Duration.ofMinutes(10), 5000) // TTL, max entries
.build();
client.check("8.8.8.8"); // API callclient.check("8.8.8.8"); // cache hit, no API callclient.invalidateCache("8.8.8.8"); // evict one entryclient.clearCache(); // evict all entriesHow it works:
- Only successful responses (
OKorWARNING) are cached - Different query flags produce separate cache entries
- Expired entries are lazily evicted on access
- When full, the least-recently-used entry is evicted
- Batch results are cached individually per address for single-lookup hits
Protect against API throttling (proxycheck.io warns at 175 req/s, denies at 200 req/s):
varclient = ProxyCheckClient.builder()
.apiKey("key")
.rateLimitPerSecond(150) // stay safely below limits
.build();| Call Type | Behavior When Exhausted |
|---|---|
| Synchronous | Blocks until the next window refill |
| Asynchronous | Fails immediately with ProxyCheckException |
Automatic retry with exponential backoff for transient failures:
// Default: 3 retries at 500ms -> 1s -> 2s (capped at 8s)varclient = ProxyCheckClient.of("key");
// Customvarclient = ProxyCheckClient.builder()
.apiKey("key")
.retryPolicy(RetryPolicy.builder()
.maxRetries(5)
.initialDelay(Duration.ofSeconds(1))
.multiplier(3.0)
.maxDelay(Duration.ofSeconds(30))
.build())
.build();
// Disablevarclient = ProxyCheckClient.builder()
.apiKey("key")
.noRetry()
.build();Retried:IOException (network failures), HTTP 5xx (server errors)
Not retried: HTTP 4xx (client errors), parse failures, API rate limit denials
Skip the API entirely for trusted addresses:
varclient = ProxyCheckClient.builder()
.apiKey("key")
.whitelist("127.0.0.1", "::1")
.whitelist(List.of("10.0.0.1", "10.0.0.2"))
.build();
// Returns ProxyCheckResponse.empty() instantlyclient.check("127.0.0.1");Whitelisted addresses are also filtered out of batch requests before the API call is made.
Monitor every stage of the request lifecycle:
// Lambda-based buildervarlistener = CheckListener.builder()
.onRequest(addrs -> log.info("Checking {} addresses", addrs.size()))
.onResponse((addrs, resp) -> metrics.record("query_time", resp.queryTime()))
.onError((addrs, err) -> alerting.fire("proxycheck_error", err))
.onCacheHit(addr -> metrics.increment("cache.hits"))
.onRetry((attempt, cause) -> log.warn("Retry #{}: {}", attempt, cause.getMessage()))
.build();
// Register via builder or at runtimeclient.addListener(listener);
client.removeListener(listener);Or implement the interface directly (all methods are default no-ops):
publicclassMetricsListenerimplementsCheckListener {
@OverridepublicvoidonRequest(Collection<String> addresses) {
metrics.increment("api.requests");
}
@OverridepublicvoidonResponse(Collection<String> addresses, ProxyCheckResponseresponse) {
metrics.record("api.query_time", response.queryTime());
}
}Client-side format validation for fast feedback before hitting the API:
Addresses.isValidIpv4("192.168.1.1"); // trueAddresses.isValidIpv6("::1"); // trueAddresses.isValidIp("8.8.8.8"); // true (IPv4 or IPv6)Addresses.isValidEmail("user@test.com"); // trueAddresses.isValid("8.8.8.8"); // true (any format)// Throws IllegalArgumentException if invalidAddresses.requireValid("not-an-address");All errors surface as the unchecked ProxyCheckException:
try {
varresponse = client.check("8.8.8.8");
} catch (ProxyCheckExceptione) {
System.err.println("Message: " + e.getMessage());
if (e.hasHttpStatusCode()) {
System.err.println("HTTP status: " + e.httpStatusCode());
}
// Original cause (IOException, InterruptedException, etc.)System.err.println("Cause: " + e.getCause());
}This library ships with a module-info.java for JPMS-based applications:
moduleyour.app {
requiresio.proxycheck.api;
}Exported packages:
| Package | Contents |
|---|---|
io.proxycheck.api | ProxyCheckClient, QueryFlags, RetryPolicy, CheckListener, Addresses |
io.proxycheck.api.model | ProxyCheckResponse, IpResult, EmailResult, Result, enums |
io.proxycheck.api.exception | ProxyCheckException |
importio.proxycheck.api.*;
importio.proxycheck.api.model.*;
importjava.time.Duration;
importjava.util.List;
publicclassExample {
publicstaticvoidmain(String[] args) {
try (varclient = ProxyCheckClient.builder()
.apiKey("your-api-key")
.cache(Duration.ofMinutes(5))
.rateLimitPerSecond(150)
.whitelist("127.0.0.1", "::1")
.listener(CheckListener.builder()
.onError((addrs, err) ->
System.err.println("Error: " + err.getMessage()))
.build())
.build()) {
// Check mixed IPs and emails in one batchvarresponse = client.check(
List.of("8.8.8.8", "1.1.1.1", "user@tempmail.org"),
QueryFlags.detailed()
);
if (!response.isSuccessful()) {
System.err.println("API error: " + response.message());
return;
}
// Block threatsresponse.threatIps().forEach(ip ->
System.out.printf("BLOCKED %s risk=%d country=%s provider=%s%n",
ip.address(), ip.detections().risk(),
ip.countryCode(), ip.provider()));
// Reject disposable emailsresponse.disposableEmails().forEach(email ->
System.out.printf("REJECTED %s (disposable)%n", email.address()));
// Pattern matching on all resultsresponse.streamResults().forEach(result -> {
switch (result) {
caseIpResultip -> System.out.printf(
"IP %-15s risk=%-9s action=%s%n",
ip.address(), ip.riskLevel(), ip.accessRecommendation());
caseEmailResultemail -> System.out.printf(
"Email %-30s disposable=%s%n",
email.address(), email.isDisposable());
}
});
}
}
}io.proxycheck.api
├── ProxyCheckClient Main client: sync/async checks, caching, retries
├── ProxyCheckClient.Builder Fluent builder for client configuration
├── QueryFlags Query parameter builder (days, node, short, tag, etc.)
├── RetryPolicy Exponential backoff configuration
├── CheckListener Observer interface for lifecycle events
├── ResponseCache TTL + LRU eviction cache (package-private)
├── RateLimiter Token-bucket rate limiter (package-private)
├── ResponseParser JSON-to-model deserialization (package-private)
├── Addresses IPv4/IPv6/email validation utilities
└── Example Comprehensive usage examples
io.proxycheck.api.model
├── ProxyCheckResponse Top-level response with filtering and streaming
├── Result (sealed) Base type for pattern matching
│ ├── IpResult Full IP check result (record)
│ └── EmailResult Email check result (record)
├── Network ASN, range, provider, type
├── Location Geo data: continent → city + coordinates
├── Currency ISO 4217 currency from location
├── DeviceEstimate Device count behind IP/subnet
├── Detections Threat flags + risk/confidence scores
├── DetectionHistory Listing/delisting status
├── AttackHistory Attack type → count map
├── Operator VPN/proxy provider metadata
├── OperatorPolicies Provider policy flags
├── NetworkType (enum) RESIDENTIAL | BUSINESS | WIRELESS | HOSTING
├── RiskLevel (enum) LOW | MEDIUM | HIGH | VERY_HIGH
├── AccessRecommendation ALLOW | CHALLENGE | DENY
├── ResponseStatus (enum) OK | WARNING | DENIED | ERROR
└── StatusMessage (enum) Known API warning/error messages
io.proxycheck.api.exception
└── ProxyCheckException Unchecked exception with optional HTTP status code
./gradlew testThe test suite contains 164 tests covering all client components, model parsing, caching behavior, rate limiting, retry logic, and edge cases.
| Dependency | Version | Purpose |
|---|---|---|
| Gson | 2.13.2 | JSON deserialization |
| JUnit 5 | 5.14.1 | Testing (test scope only) |
No transitive dependencies. No frameworks. No annotation processors.
Contributions are welcome! Here's how to get started:
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Write tests for your changes
- Ensure all tests pass (
./gradlew test) - Commit with a clear message (
git commit -m "Add support for ...") - Push to your branch (
git push origin feature/my-feature) - Open a Pull Request
- Follow existing code style and naming conventions
- Maintain backward compatibility for public APIs
- Add Javadoc for all new public methods and classes
- Keep the single-dependency philosophy — avoid adding new runtime dependencies
- Target Java 21+ features (records, sealed types, pattern matching)
This project is licensed under the GNU General Public License v3.0 — see the LICENSE file for details.
Built with Java 21 • Powered by proxycheck.io