The EZCaptchaSolver Java SDK is an open-source Java client maintained by EZXLabs for its CAPTCHA recognition task API. It provides typed requests and a thread-safe client for the supported task types below; the library also includes a TLS forwarding task that does not solve CAPTCHAs. For the wider SDK family, see the EZCaptchaSolver SDK product page; for HTTP request and response fields, see the EZCaptchaSolver API reference; for Java usage, see the examples in this repository.
Captcha task types come in a synchronous and an asynchronous form:
- Synchronous: the request blocks after the task is created and returns once the task is done.
- Asynchronous: creating the task returns a task ID, and the result is fetched later by polling that ID. This suits captcha types that take a while to solve.
A captcha type can support both forms at once, and almost every type supports the synchronous one. A few types are asynchronous only.
| Task type | Modes | Example | Description |
|---|---|---|---|
ReCaptchaV2TaskProxyless |
all | Example | reCAPTCHA v2 |
ReCaptchaV2TaskProxylessS9 |
all | Example | reCAPTCHA v2, returns a token scored ≥ 0.9 |
ReCaptchaV2STaskProxyless |
all | Example | reCAPTCHA v2 carrying the challenge-bound s parameter |
ReCaptchaV2EnterpriseTaskProxyless |
all | Example | reCAPTCHA v2 Enterprise |
ReCaptchaV2SEnterpriseTaskProxyless |
all | Example | reCAPTCHA v2 Enterprise, carrying the s parameter |
ReCaptchaV2Classification |
sync | Example | reCAPTCHA v2 image recognition |
| Task type | Modes | Example | Description |
|---|---|---|---|
ReCaptchaV3TaskProxyless |
all | Example | reCAPTCHA v3 |
ReCaptchaV3TaskProxylessS9 |
all | Example | reCAPTCHA v3, returns a token scored ≥ 0.9 |
ReCaptchaV3EnterpriseTaskProxyless |
all | Example | reCAPTCHA v3 Enterprise |
ReCaptchaV3EnterpriseTaskProxylessS9 |
all | Example | reCAPTCHA v3 Enterprise, returns a token scored ≥ 0.9 |
| Task type | Modes | Example | Description |
|---|---|---|---|
FuncaptchaTaskProxyless |
async | Example | FunCaptcha / Arkose Labs |
FunCaptchaClassification |
sync | Example | FunCaptcha image recognition |
| Task type | Modes | Example | Description |
|---|---|---|---|
HCaptcha |
async | Example | hCaptcha |
HCaptchaClassification |
sync | Example | hCaptcha image recognition, single or multiple images |
| Task type | Modes | Example | Description |
|---|---|---|---|
CloudFlare5STask |
async | Example | CF five-second interstitial, requires a proxy |
CloudFlareTurnstileTask |
async | Example | Turnstile, returns a token |
| Task type | Modes | Example | Description |
|---|---|---|---|
AkamaiWEBTaskProxyless |
sync | Example | Akamai Web |
AkamaiSBSDTaskProxyless |
sync | Example | Akamai SBSD |
Akamai Web is a multi-round flow: feed the
encodedataof one round back as theencodeDataof the next. The two spellings genuinely differ on the wire; the SDK keeps the service's definitions as they are rather than "fixing" them.
| Task type | Modes | Example | Description |
|---|---|---|---|
DataDomeTaskProxyless |
sync | Example | The challenge after an interception, in two steps selected by step |
DataDomeTagsTaskProxyless |
sync | Example | Reports a fingerprint on the normal browsing path |
| Task type | Modes | Example | Description |
|---|---|---|---|
PerimeterX |
async | Example | PerimeterX clearance cookies |
IncapsulaTaskProxyless |
sync | Example | Incapsula Reese84 payload |
TlsTask |
sync | Example | HTTP request forwarded over TLS, returns the upstream response |
Maven
<dependency>
<groupId>com.burstlinker.ezcapsolver</groupId>
<artifactId>ezcapsolver-java</artifactId>
<version>0.1.0</version>
</dependency>Gradle
implementation("com.burstlinker.ezcapsolver:ezcapsolver-java:0.1.0")Java 8 or newer. The artifact is compiled with --release 8 and CI runs the whole test suite on a real JDK 8 as well, so the floor is tested rather than merely declared.
The client reads EZCAPTCHA_API_KEY from the environment when the configuration carries no explicit key.
import com.burstlinker.ezcapsolver.EzCapSolverClient;
import com.burstlinker.ezcapsolver.model.Solved;
import com.burstlinker.ezcapsolver.model.solution.ReCaptchaSolution;
import com.burstlinker.ezcapsolver.model.task.ReCaptchaV2TaskParams;
EzCapSolverClient client = new EzCapSolverClient();
ReCaptchaV2TaskParams params = ReCaptchaV2TaskParams.builder()
.websiteUrl("https://example.com")
.websiteKey("6Lc_your_site_key")
.build();
Solved<ReCaptchaSolution> solved = client.solveReCaptchaV2TaskProxyless(params);
System.out.println(solved.getTaskId());
System.out.println(solved.getSolution().getToken());Creating a task is billed, and it is billed whether or not the worker succeeds. The SDK never retries task creation on its own: a timeout cannot tell you whether the service already accepted the task, so a blind retry pays twice. See Errors for what to do instead.
Every task type has two methods, taking the same parameter model and returning the same type. Only the endpoint differs:
// Create, then poll for the result
Solved<ReCaptchaSolution> a = client.solveReCaptchaV2TaskProxyless(params);
// The synchronous endpoint, answered in one request
Solved<ReCaptchaSolution> b = client.syncSolveReCaptchaV2TaskProxyless(params);The first five types share ReCaptchaV2TaskParams and ReCaptchaSolution; only the method name differs.
ReCaptchaV2TaskParams params = ReCaptchaV2TaskParams.builder()
.websiteUrl("https://example.com")
.websiteKey("6Lc_your_site_key")
.invisible(false)
.build();
Solved<ReCaptchaSolution> solved = client.solveReCaptchaV2TaskProxyless(params);
System.out.println(solved.getSolution().getToken());invisible is a primitive boolean, so it is always sent. That is the point: false is a real answer meaning "the widget is visible", and omitting the field would let the service apply its own default instead of your choice.
Same parameters as plain v2, on the high-score queue, returning a token scored 0.9 or above.
Solved<ReCaptchaSolution> solved = client.solveReCaptchaV2TaskProxylessS9(params);
System.out.println(solved.getSolution().getToken());Carries the challenge-bound s parameter. It is not actually mandatory; leaving it out behaves like plain v2.
ReCaptchaV2TaskParams params = ReCaptchaV2TaskParams.builder()
.websiteUrl("https://example.com")
.websiteKey("6Lc_your_site_key")
.s("value-read-from-the-page")
.build();
Solved<ReCaptchaSolution> solved = client.solveReCaptchaV2STaskProxyless(params);Solved<ReCaptchaSolution> solved = client.solveReCaptchaV2EnterpriseTaskProxyless(params);Solved<ReCaptchaSolution> solved = client.solveReCaptchaV2SEnterpriseTaskProxyless(params);Reads an image grid rather than solving the widget. Synchronous.
byte[] image = Files.readAllBytes(Paths.get("grid.jpg"));
ReCaptchaV2ClassificationTaskParams params = ReCaptchaV2ClassificationTaskParams.builder()
.image(Base64.getEncoder().encodeToString(image))
.question("/m/014xcs")
.size(3)
.build();
Solved<ReClassificationSolution> solved =
client.syncSolveReCaptchaV2Classification(params);
System.out.println(solved.getSolution().getObjects()); // e.g. [0, 4, 7]
System.out.println(solved.getSolution().isMulti());size is a boxed Integer, so leaving it unset omits it and the service applies its own default of 4. Set it only when the grid is not 4×4.
All four share ReCaptchaV3TaskParams and ReCaptchaSolution.
ReCaptchaV3TaskParams params = ReCaptchaV3TaskParams.builder()
.websiteUrl("https://example.com/checkout")
.websiteKey("6Lc_your_site_key")
.pageAction("checkout")
.build();
Solved<ReCaptchaSolution> solved = client.solveReCaptchaV3TaskProxyless(params);
System.out.println(solved.getSolution().getToken());Solved<ReCaptchaSolution> solved = client.solveReCaptchaV3TaskProxylessS9(params);Solved<ReCaptchaSolution> solved = client.solveReCaptchaV3EnterpriseTaskProxyless(params);Solved<ReCaptchaSolution> solved = client.solveReCaptchaV3EnterpriseTaskProxylessS9(params);FunCaptchaTaskParams params = FunCaptchaTaskParams.builder()
.websiteUrl("https://example.com/signup")
.websiteKey("YOUR_PUBLIC_KEY")
.apiJsSubdomain("client-api.arkoselabs.com")
.data("blob-value-from-the-page")
.build();
Solved<FunCaptchaSolution> solved = client.solveFuncaptchaTaskProxyless(params);
System.out.println(solved.getSolution().getToken());FunCaptchaClassificationTaskParams params = FunCaptchaClassificationTaskParams.builder()
.image(Base64.getEncoder().encodeToString(image))
.question("Pick the image that is the right way up")
.build();
Solved<FunCaptchaClassificationSolution> solved =
client.syncSolveFunCaptchaClassification(params);
System.out.println(solved.getRaw());HCaptchaTaskParams params = HCaptchaTaskParams.builder()
.websiteUrl("https://accounts.hcaptcha.com/demo")
.websiteKey("338af34c-7bcb-4c7c-900b-acbec73d7d43")
.lang("en-US")
.invisible(false)
.build();
Solved<HCaptchaSolution> solved = client.solveHCaptcha(params);
System.out.println(solved.getSolution().getGeneratedPassUuid());
System.out.println(solved.getSolution().getUa());HCaptchaClassificationTaskParams params = HCaptchaClassificationTaskParams.builder()
.images(Arrays.asList(imageBase64))
.question("Please click each image containing a crosswalk")
.build();
Solved<HCaptchaClassificationSolution> solved =
client.syncSolveHCaptchaClassification(params);
System.out.println(solved.getRaw());Map<String, Object> rqData = new LinkedHashMap<>();
rqData.put("chlPageData", "...");
CloudFlare5sTaskParams params = CloudFlare5sTaskParams.builder()
.websiteUrl("https://example.com")
.proxy("http://user:pass@host:8080")
.rqData(rqData)
.build();
Solved<CloudFlare5sSolution> solved = client.solveCloudFlare5STask(params);
System.out.println(solved.getSolution().getCookies());
System.out.println(solved.getSolution().getHeader());
System.out.println(solved.getSolution().getTlsVersion());The proxy is required for this type, unlike most others.
CloudFlareTurnstileTaskParams params = CloudFlareTurnstileTaskParams.builder()
.websiteUrl("https://example.com/login")
.websiteKey("0x4AAAAAAA...")
.build();
Solved<CloudFlareTurnstileSolution> solved = client.solveCloudFlareTurnstileTask(params);
System.out.println(solved.getSolution().getToken());A multi-round flow. Each round is a separate billed task.
String encodeData = "";
for (int index = 0; index < 3; index++) {
AkamaiWebTaskParams params = AkamaiWebTaskParams.builder()
.pageUrl("https://example.com")
.v3Url("https://example.com/akam/13/abcdef12")
.ua("Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
.lang("en-US")
.index(index)
.encodeData(encodeData)
.build();
Solved<AkamaiWebSolution> solved = client.syncSolveAkamaiWEBTaskProxyless(params);
// encodedata out, encodeData in — the service spells it differently per direction
encodeData = solved.getSolution().getEncodedata();
}AkamaiSbsdTaskParams params = AkamaiSbsdTaskParams.builder()
.pageUrl("https://example.com")
.sbsdUrl("https://example.com/.well-known/sbsd")
.bmSo("existing-bm_so-cookie")
.ua("Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
.lang("en-US")
.scriptBase64(Base64.getEncoder().encodeToString(script))
.build();
Solved<AkamaiSbsdSolution> solved = client.syncSolveAkamaiSBSDTaskProxyless(params);
System.out.println(solved.getSolution().getPayload());DataDomeTaskParams params = DataDomeTaskParams.builder()
.htmlB64(Base64.getEncoder().encodeToString(challengeHtml))
.step(DataDomeTaskParams.STEP_ONE)
.referer("https://example.com/search")
.build();
Solved<DataDomeSolution> solved = client.syncSolveDataDomeTaskProxyless(params);
System.out.println(solved.getSolution().getUrl());Map<String, Object> fields = new LinkedHashMap<>();
fields.put("tags_url", "https://example.com/js/tags.js");
DataDomeTagsTaskParams params = DataDomeTagsTaskParams.builder()
.ddk("YOUR_DDJSKEY") // window.ddjskey on the page
.jsType(DataDomeTagsTaskParams.JS_TYPE_CH)
.bpc(DataDomeTagsTaskParams.MIN_PACKET_COUNTER)
.referer("https://example.com/search")
.ua("Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
.fields(fields)
.build();
Solved<DataDomeSolution> solved = client.syncSolveDataDomeTagsTaskProxyless(params);PerimeterXTaskParams params = PerimeterXTaskParams.builder()
.websiteKey("YOUR_PX_APP_ID")
.invisible(false)
.build();
Solved<PerimeterXSolution> solved = client.solvePerimeterX(params);
System.out.println(solved.getSolution().getPx3());
System.out.println(solved.getSolution().getPxVid());
System.out.println(solved.getSolution().getPxde());IncapsulaTaskParams params = IncapsulaTaskParams.builder()
.script("(function(){...})()") // the script source, not just its URL
.scriptUrl("https://example.com/_Incapsula_Resource?SWJIYLWA=...")
.pageUrl("https://example.com")
.ua("Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
.acceptLanguage("en-US,en;q=0.9")
.build();
Solved<IncapsulaSolution> solved = client.syncSolveIncapsulaTaskProxyless(params);
System.out.println(solved.getSolution().getData());Not a captcha at all: a worker performs one HTTP request with its own TLS fingerprint and hands back the upstream response.
Map<String, Object> headers = new LinkedHashMap<>();
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
headers.put("Accept", "application/json");
TlsForwardTaskParams params = TlsForwardTaskParams.builder()
.tlsType("chrome")
.proxy("http://user:pass@host:8080")
.method(TlsForwardTaskParams.METHOD_GET)
.url("https://example.com/api/profile")
.headers(headers)
.headersOrder("User-Agent,Accept")
.build();
Solved<TlsForwardSolution> solved = client.syncSolveTlsTask(params);
System.out.println(solved.getSolution().getStatus());
System.out.println(solved.getSolution().getBody());The service adds parameters faster than any SDK releases. You never have to wait for one.
Any parameter a model does not declare goes through put, and lands at the same level as the declared fields on the wire:
HCaptchaTaskParams params = HCaptchaTaskParams.builder()
.websiteUrl("https://example.com")
.websiteKey("...")
.build();
params.put("someParameterAddedLater", 42);The same applies in reverse. Anything a worker returns that the solution model does not declare is kept rather than discarded:
Solved<HCaptchaSolution> solved = client.solveHCaptcha(params);
Object extra = solved.getSolution().get("someNewField");
JsonNode raw = solved.getRaw(); // the whole response, untouchedA task type the service added after this release still works — pass the wire name as a string:
Map<String, Object> params = new LinkedHashMap<>();
params.put("websiteURL", "https://example.com");
params.put("websiteKey", "...");
Solved<JsonNode> solved = client.solveRaw("SomeBrandNewTaskType", params);
Solved<JsonNode> sync = client.syncSolveRaw("SomeBrandNewTaskType", params);TaskType holds the known names as constants, but it is a holder of Strings rather than an enum precisely so that an unknown type is a normal call, not a compile error. TaskType.isKnown(name) tells you whether this release models one.
Shortest first:
new EzCapSolverClient(); // defaults, key from EZCAPTCHA_API_KEY
new EzCapSolverClient("your-key"); // defaults, key given here
new EzCapSolverClient(someClientConfig); // a configuration you already built
EzCapSolverClient.builder()./* ... */.build(); // configured inlineEzCapSolverClient.of("your-key") and EzCapSolverClient.of(someClientConfig) are the same two constructors under another name. They all end up in the same place; pick whichever reads best at the call site.
EzCapSolverClient client = EzCapSolverClient.builder()
.clientKey("your-key")
.timeout(Duration.ofSeconds(30))
.syncTimeout(Duration.ofSeconds(240))
.pollInterval(Duration.ofSeconds(3))
.maxPollAttempts(50)
.userAgent("my-app/1.0")
.build();| Option | Default | Notes |
|---|---|---|
clientKey |
$EZCAPTCHA_API_KEY |
Required, from the environment if not set here |
timeout |
30s | Asynchronous endpoints and the balance query |
syncTimeout |
240s | /createSyncTask only |
pollInterval |
3s | Wait between result queries |
maxPollAttempts |
50 | pollInterval × maxPollAttempts is the longest solve* waits |
appId |
— | Optional developer/affiliate identifier |
proxy |
— | How your process reaches EZCaptchaSolver, not the worker's proxy |
userAgent |
ezcapsolver-java/<version> |
|
asyncBaseUrl / syncBaseUrl |
the public endpoints | Override for a mock or a private deployment |
okHttpClient |
a fresh one | Supply your own to share a connection pool |
The two timeout budgets are separate on purpose. timeout covers endpoints that answer immediately; syncTimeout covers /createSyncTask, which blocks until a worker finishes — the service allows some types three minutes. One shared value means either cutting off a synchronous task that was about to succeed (already paid for), or waiting minutes for a balance query that should have failed in seconds.
ClientConfig.toString() masks the client key and the proxy URL, a proxy URL carrying credentials of its own. It is safe to log.
One client is enough for an entire application. It is immutable after construction and holds a single OkHttp connection pool; building one per request throws that pool away and opens fresh connections every time. It is safe to share across threads.
Every failure is an unchecked exception under EzCaptchaException, so catching that one type is a complete safety net.
| Exception | Meaning |
|---|---|
EzCaptchaException |
The base class. Configuration errors are thrown as this directly |
TransportException |
Never reached the service, or no answer came back |
ApiException |
The service answered, and said no |
PollingExhaustedException |
The polling budget ran out. The task is still running |
UnexpectedResponseException |
A well-formed response that broke the contract |
SolutionDecodeException |
The task succeeded; the solution would not decode |
WaitInterruptedException |
The thread was interrupted mid-wait |
On ApiException, three predicates cover what you actually need to decide:
try {
Solved<HCaptchaSolution> solved = client.solveHCaptcha(params);
} catch (ApiException e) {
if (e.isAuthenticationError()) {
// Stop. These codes trip a server-side ban counter; retrying digs the hole deeper.
} else if (e.isRateLimited()) {
// Throttled before anything was created. Safe to retry after a wait.
} else if (e.isTerminal()) {
// Retrying with the same input fails the same way. Change the request.
}
log.error("{} ({}) request={}", e.getErrorCode(), e.getHttpStatus(), e.getRequestId());
}This is what the hierarchy is really for. When a wait fails, the task usually still exists — and the service holds its result for five minutes.
try {
solved = client.solveHCaptcha(params);
} catch (EzCaptchaException e) {
String taskId = EzCaptchaException.taskIdOf(e); // works on any of them
if (taskId != null) {
solved = client.waitForResult(taskId, HCaptchaSolution.class);
}
}taskIdOf is a single entry point precisely so this works without knowing which layer failed. Creating a second task instead means paying twice for the same work.
The SDK logs through slf4j and ships only the API. It stays silent until you put a binding on the classpath — whichever your application already uses:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.3.15</version>
</dependency>Logback 1.3.x is the Java 8 line; 1.4+ requires Java 11.
Loggers are named after their classes, so com.burstlinker.ezcapsolver scopes the whole SDK. Nothing it logs contains your API key.
One file per task type, in examples — indexed there, with the code under com.burstlinker.ezcapsolver.examples.
export EZCAPTCHA_API_KEY=your-key
mvn -q compile
mvn -q dependency:build-classpath -Dmdep.outputFile=target/cp.txt
java -cp "target/classes:$(cat target/cp.txt)" \
com.burstlinker.ezcapsolver.examples.hcaptcha.HCaptchaThe examples live in the source tree so the compiler checks them on every build, and are excluded from the published jar, sources jar and javadoc.
mvn clean test # the suite
JAVA_HOME=/path/to/jdk8 mvn clean test # again on Java 8
mvn verify # adds animal-sniffer and japicmp
mvn javadoc:javadoc
typosNo test is allowed to reach the real service — creating a task is billed. Every HTTP test goes through MockWebServer.
See CONTRIBUTING.md for the five places a new task type touches, and for the two model traps that are easy to hit.