This client library allows JVM-based applications to verify Friendly Captcha puzzle solutions. It wraps the necessary call and interprets the result.
- Easy to use (see example below)
- Requires Java 17 or later
- Compatible with JVM-based applications (Java, Groovy, Kotlin, Scala, Clojure)
- Supports both Friendly Captcha API v1 and v2
- Uses the built-in Java HTTP client — no extra HTTP library dependency
- Only two runtime dependencies: Jackson 3 (
tools.jackson.core:jackson-databind) and SLF4J
The official FriendlyCaptcha/friendly-captcha-jvm SDK is the Friendly Captcha team's own library. It even recommends this library for API v1 support. Here is how the two compare:
| Feature | this library | friendly-captcha-jvm |
|---|---|---|
| API v1 support | Yes | No (v2 only) |
| API v2 support | Yes | Yes |
| Proxy support (host, port, auth) | Yes | No |
| Connect / request timeout | Yes | No |
| Regional / custom endpoint | Yes | No |
Custom User-Agent |
Yes | No |
| Verbose SLF4J logging | Yes | No |
| HTTP client | Built-in Java HttpClient |
Separate HTTP library |
| API style | Synchronous and async (CompletableFuture) |
Asynchronous only (CompletableFuture) |
| Risk intelligence retrieval | No | Yes |
| Minimum Java version | 17 | 8 |
| License | LGPL | MIT |
Include the dependency using Maven:
<dependency>
<groupId>org.drjekyll</groupId>
<artifactId>friendlycaptcha</artifactId>
<version>3.0.0</version>
</dependency>or Gradle with Groovy DSL:
implementation 'org.drjekyll:friendlycaptcha:3.0.0'or Gradle with Kotlin DSL:
implementation("org.drjekyll:friendlycaptcha:3.0.0")Jackson 3 note: This library depends on Jackson 3 (
tools.jackson.core:jackson-databind). If your project currently uses Jackson 2 (com.fasterxml.jackson.core), you will need to migrate to Jackson 3 or manage both versions on the classpath.
Friendly Captcha API v2 is the current recommended version. The API key is sent as the
X-API-Key request header and the solution as the response body parameter to
https://global.frcapi.com/api/v2/captcha/siteverify.
import org.drjekyll.friendlycaptcha.FriendlyCaptchaException;
import org.drjekyll.friendlycaptcha.FriendlyCaptchaVerifier;
import org.drjekyll.friendlycaptcha.FriendlyCaptchaVersion;
public class FriendlyCaptchaV2Example {
private final FriendlyCaptchaVerifier friendlyCaptchaVerifier = FriendlyCaptchaVerifier
.builder()
.version(FriendlyCaptchaVersion.V2)
.apiKey("YOUR_API_KEY")
.sitekey("AN_OPTIONAL_SITE_KEY")
.build();
public void checkSolution(String solution) {
try {
boolean success = friendlyCaptchaVerifier.verify(solution);
if (success) {
// continue
} else {
// solution invalid, expired, or already used — reject the submission
}
} catch (FriendlyCaptchaException e) {
// API or network error — log and decide whether to fail open or closed
}
}
}Or Kotlin:
import org.drjekyll.friendlycaptcha.FriendlyCaptchaException
import org.drjekyll.friendlycaptcha.FriendlyCaptchaVerifier
import org.drjekyll.friendlycaptcha.FriendlyCaptchaVersion
class FriendlyCaptchaV2Example {
private val friendlyCaptchaVerifier: FriendlyCaptchaVerifier = FriendlyCaptchaVerifier
.builder()
.version(FriendlyCaptchaVersion.V2)
.apiKey("YOUR_API_KEY")
.sitekey("AN_OPTIONAL_SITE_KEY")
.build()
fun checkSolution(solution: String?) {
try {
val success: Boolean = friendlyCaptchaVerifier.verify(solution)
if (success) {
// continue
} else {
// solution invalid, expired, or already used — reject the submission
}
} catch (e: FriendlyCaptchaException) {
// API or network error — log and decide whether to fail open or closed
}
}
}Friendly Captcha API v1 is the default when no .version(...) is set. The API key is sent as
the secret form field and the solution as the solution form field to
https://api.friendlycaptcha.com/api/v1/siteverify.
import org.drjekyll.friendlycaptcha.FriendlyCaptchaException;
import org.drjekyll.friendlycaptcha.FriendlyCaptchaVerifier;
public class FriendlyCaptchaExample {
private final FriendlyCaptchaVerifier friendlyCaptchaVerifier = FriendlyCaptchaVerifier
.builder()
.apiKey("YOUR_API_KEY")
.sitekey("AN_OPTIONAL_SITE_KEY")
.build();
public void checkSolution(String solution) {
try {
boolean success = friendlyCaptchaVerifier.verify(solution);
if (success) {
// continue
} else {
// solution invalid, expired, or already used — reject the submission
}
} catch (FriendlyCaptchaException e) {
// API or network error — log and decide whether to fail open or closed
}
}
}Or Kotlin:
import org.drjekyll.friendlycaptcha.FriendlyCaptchaException
import org.drjekyll.friendlycaptcha.FriendlyCaptchaVerifier
class FriendlyCaptchaExample {
private val friendlyCaptchaVerifier: FriendlyCaptchaVerifier = FriendlyCaptchaVerifier
.builder()
.apiKey("YOUR_API_KEY")
.sitekey("AN_OPTIONAL_SITE_KEY")
.build()
fun checkSolution(solution: String?) {
try {
val success: Boolean = friendlyCaptchaVerifier.verify(solution)
if (success) {
// continue
} else {
// solution invalid, expired, or already used — reject the submission
}
} catch (e: FriendlyCaptchaException) {
// API or network error — log and decide whether to fail open or closed
}
}
}verify(solution) has three possible outcomes, regardless of whether you use v1 or v2:
| Outcome | When |
|---|---|
Returns true |
The solution is valid and was accepted |
Returns false |
The solution is invalid, expired, or already used |
Throws FriendlyCaptchaException |
The API rejected the request itself (bad API key, malformed request, network error, unreadable response) |
verify also throws IllegalArgumentException if the solution or API key is null or empty.
verifyAsync(solution) returns a CompletableFuture<Boolean> and uses the non-blocking
HttpClient.sendAsync under the hood — no thread is blocked while the request is in flight.
friendlyCaptchaVerifier.verifyAsync(solution)
.thenAccept(success -> {
if (success) {
// continue
} else {
// solution invalid, expired, or already used — reject the submission
}
})
.exceptionally(ex -> {
Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
if (cause instanceof FriendlyCaptchaException fce && fce.getStatusCode() != null
&& fce.getStatusCode() == 503) {
// API temporarily unavailable — fail open
} else {
// permanent error — log and handle
}
return null;
});The future completes exceptionally with a CompletionException whose cause is always a
FriendlyCaptchaException — network failures are wrapped in one just like the synchronous
verify method. The same getStatusCode() / getErrorCode() introspection described below
applies to the unwrapped cause.
FriendlyCaptchaException exposes two optional details:
getStatusCode()— the HTTP status code returned by the API, ornullfor non-HTTP failures (network errors, unreadable responses, invalid configuration).getErrorCode()— the machine-readableErrorCodefrom the response body, ornullwhen the API did not include one.
Retrying on 503 (service unavailable)
A 503 response means the Friendly Captcha API was temporarily unavailable. In this case it is safe to fail open (accept the submission) rather than blocking the user, and schedule a retry later:
try {
boolean success = friendlyCaptchaVerifier.verify(solution);
if (!success) {
// reject
}
} catch (FriendlyCaptchaException e) {
if (e.getStatusCode() != null && e.getStatusCode() == 503) {
// API temporarily unavailable — fail open and retry later
log.warn("Friendly Captcha API unavailable (503), failing open", e);
} else {
// Permanent error — check credentials and request format
throw e;
}
}Evaluating the error code for troubleshooting
When getErrorCode() is non-null you can branch on the specific ErrorCode constant for
fine-grained error handling or logging:
} catch (FriendlyCaptchaException e) {
ErrorCode code = e.getErrorCode();
if (code == ErrorCode.AUTH_INVALID || code == ErrorCode.SECRET_INVALID) {
log.error("API key is invalid — check your Friendly Captcha account settings");
} else if (code == ErrorCode.SITEKEY_INVALID) {
log.error("Sitekey mismatch — ensure the widget sitekey matches the verifier");
} else if (e.getStatusCode() != null && e.getStatusCode() == 503) {
log.warn("Friendly Captcha API temporarily unavailable (503), failing open");
} else {
log.error("Captcha verification failed: {} (HTTP {})", code, e.getStatusCode(), e);
}
}The full set of error codes is documented in the ErrorCode enum Javadoc and in the
Friendly Captcha API reference.
The v2 API offers regional endpoints. Pass a custom URI via .verificationEndpoint(...):
FriendlyCaptchaVerifier verifier = FriendlyCaptchaVerifier.builder()
.version(FriendlyCaptchaVersion.V2)
.apiKey("YOUR_API_KEY")
.verificationEndpoint(URI.create("https://eu.frcapi.com/api/v2/captcha/siteverify"))
.build();Version 3.0.0 requires Java 17 or later. If your project still targets Java 8, stay on the 2.x release line.
Version 3.0.0 upgraded the Jackson dependency from Jackson 2 (com.fasterxml.jackson.core) to
Jackson 3 (tools.jackson.core). If your project still uses Jackson 2, you will need to either
migrate alongside this library or continue on the 2.x release line.
FriendlyCaptchaVerifier.builder() supports the following methods:
| Parameter | Description |
|---|---|
.apiKey(...) |
Required. The API key from your Friendly Captcha account. |
.version(...) |
FriendlyCaptchaVersion.V1 (default) or FriendlyCaptchaVersion.V2 (recommended). For v1, the API key is sent as the secret form field. For v2, it is sent as the X-API-Key request header. |
.sitekey(...) |
Optional sitekey to verify that the puzzle was generated for your site. |
.verificationEndpoint(...) |
Custom verification endpoint URI. Defaults to https://api.friendlycaptcha.com/api/v1/siteverify for v1 and https://global.frcapi.com/api/v2/captcha/siteverify for v2. Use https://eu.frcapi.com/api/v2/captcha/siteverify for EU-only data residency (v2). |
.connectTimeout(...) |
Connection establishment timeout (Duration). null uses the system default, Duration.ZERO means infinite. |
.socketTimeout(...) |
Total request timeout (Duration) covering the entire request from sending to receiving the full response. null means no timeout. |
.objectMapper(...) |
Custom Jackson 3 ObjectMapper instance. If not set, a default ObjectMapper is used. |
.proxyHost(...) |
Hostname or IP address of an HTTP proxy. proxyPort must also be set. |
.proxyPort(...) |
Port of an HTTP proxy. proxyHost must also be set. |
.proxyUserName(...) |
Username for HTTP proxy basic authentication. proxyHost, proxyPort, and proxyPassword must also be set. |
.proxyPassword(...) |
Password for HTTP proxy basic authentication. proxyHost, proxyPort, and proxyUserName must also be set. |
.userAgent(...) |
Custom User-Agent header value sent with every request. Defaults to FriendlyCaptchaJavaClient. |
.verbose(true) |
Logs endpoint and response details at INFO level via SLF4J. |
To build and locally install the library and run the tests, just call
mvn installPlease read the contribution document for details on our code of conduct, and the process for submitting pull requests to us.
We use SemVer for versioning. For the versions available, see the tags on this repository.
This project is licensed under the LGPL License - see the license file for details.
- Requires Java 17 — dropped support for Java 8
- Upgraded to Jackson 3 (
tools.jackson.core:jackson-databind:3.x) — Jackson 2 is no longer a transitive dependency - Replaced
HttpURLConnectionwith the built-in JavaHttpClient(java.net.http) — no third-party HTTP library required socketTimeoutnow covers the entire request duration (connect + send + receive) instead of the per-read socket timeout- Single
FriendlyCaptchaVerifier.builder()entry point — select the API version via.version(FriendlyCaptchaVersion.V1)(default) or.version(FriendlyCaptchaVersion.V2) - Added support for Friendly Captcha API v2: sends the API key as the
X-API-Keyheader, uses theresponsebody parameter, and parses the v2 response format - Fixed sitekey not being URL-encoded in the POST body
- New
.userAgent(...)builder parameter to override the defaultUser-Agentheader - Added
verifyAsync(solution)returningCompletableFuture<Boolean>for non-blocking verification
Dependency updates
Dependency updates
Got rid of HTTP client dependency. Apache HTTP Client is no longer needed.
- Update dependencies
- Add verbose logging
- Add proxy authentication
- Initial version