Use the Go SDK with chromedp
Solve first, then launch Chrome with the same proxy and the returned browser identity. Insert the token into the application's form, submit it, and wait for the application's result. A ready task means you have a solution—not that the website has accepted it.
On this page
Set up the modules, profile and proxy
Install solvethree.com/[email protected] and github.com/chromedp/[email protected] with go get. The SDK requires Go 1.22+, but this chromedp version requires Go 1.25+, so use that for the combined example. Its typed API differs from older chromedp examples.
Create an active site profile for the target hostname in your workspace before running the program. Sending a hostname and action doesn't create a profile.
TARGET_URL: the complete HTTPS page URL containing your form. The task uses its hostname asSite; Chrome navigates to the complete URL.SOLVETHREE_PROXY: a single HTTP or HTTPS proxy URL. This example requires an IP-allowlisted proxy without credentials in the URL.SOLVETHREE_API_KEY: your workspace API key. Keep it in the Go process, not in page JavaScript.
The example requests the login action and uses the default Enterprise v3 reCAPTCHA mode. Change the action to the value your application expects.
Solve, launch Chrome, and submit
The program waits for the SDK solve before creating Chrome's allocator. It configures Chrome with the same single proxy and result.Solution.UserAgent, then sets the returned SecChUA as the sec-ch-ua header before navigation. Don't substitute an unrelated browser identity or switch proxies between solving and submission.
// go get github.com/chromedp/[email protected] (requires Go 1.25+)
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/url"
"os"
"time"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
solvethree "solvethree.com/sdk"
)
func main() { if err := run(); err != nil { log.Fatal(err) } }
func run() error {
target,err:=url.Parse(os.Getenv("TARGET_URL"));if err!=nil||target.Scheme!="https"||target.Hostname()=="" { return fmt.Errorf("set an HTTPS TARGET_URL") }
proxy,err:=url.Parse(os.Getenv("SOLVETHREE_PROXY"));if err!=nil||proxy.User!=nil { return fmt.Errorf("use an IP-allowlisted proxy without URL credentials") }
client,err:=solvethree.NewClient(os.Getenv("SOLVETHREE_API_KEY"),nil);if err!=nil { return err }
ctx,cancel:=context.WithTimeout(context.Background(),180*time.Second);defer cancel()
result,err:=client.Solve(ctx,solvethree.TaskRequest{Site:target.Hostname(),Action:"login",Proxy:proxy.String()},solvethree.SolveOptions{})
if err!=nil { return err }
options:=append(chromedp.DefaultExecAllocatorOptions[:],chromedp.ProxyServer(proxy.String()),chromedp.UserAgent(result.Solution.UserAgent))
allocator,closeAllocator:=chromedp.NewExecAllocator(context.Background(),options...);defer closeAllocator()
browser,closeBrowser:=chromedp.NewContext(allocator);defer closeBrowser()
browser,stop:=context.WithTimeout(browser,60*time.Second);defer stop()
tokenJSON,err:=json.Marshal(result.Solution.Token);if err!=nil { return err }
script:=`(token => {const field=document.querySelector('[name="g-recaptcha-response"]'); field.value=token; field.dispatchEvent(new Event('input',{bubbles:true}));})(`+string(tokenJSON)+`)`
_,err=chromedp.Call(browser,network.SetExtraHTTPHeaders,network.SetExtraHTTPHeadersParams{Headers:network.Headers{"sec-ch-ua":result.Solution.SecChUA}})
if err!=nil { return err }
err=chromedp.Do(browser,
chromedp.Navigate(target.String()),chromedp.WaitReady(chromedp.CSS(`[name="g-recaptcha-response"]`)),
chromedp.Evaluate[chromedp.Void](script),chromedp.Click(chromedp.CSS(`button[type="submit"]`)),
chromedp.WaitVisible(chromedp.ID("success")))
if err!=nil { return err }
// #success is this owned demo form's outcome. Adapt its selectors, token
// parameter and success check to your application, including CSRF/session fields.
return nil
}This example deliberately uses one proxy, so there's no candidate selection to resolve. If you later use ProxyCandidates, obtain the successful proxy with SuccessfulProxy(originalTask, readyResult) and use that proxy for Chrome.
Replace the demo form assumptions
The token insertion uses json.Marshal before constructing the JavaScript expression. Keep that escaping step: it passes the token as a JavaScript string value rather than interpolating unescaped text into executable code.
- Replace
[name="g-recaptcha-response"]with the field your application actually submits. The sample sets its value and dispatches an input event; adapt this if your application needs a different submission path. - Replace
button[type="submit"]with the intended submit control. Include any required form values, CSRF fields and session state before clicking. - Replace
#successwith an outcome that genuinely indicates acceptance in your application. A completed click or a populated token field isn't that outcome.
The SDK doesn't submit the token to the target. If the target rejects the request, inspect the outgoing form first: confirm it contains result.Solution.Token, not the task ID or an older field value, and check that the proxy and browser identity still match.
Handle deadlines without creating duplicate solves
The solve has a 180-second context deadline. Chrome gets a separate 60-second deadline after solving. The run function returns errors so its deferred context cancellation and browser cleanup run before main logs an error.
Canceling the solve context stops waiting, not remote work. The SDK creates once and retries only temporary polling failures; it never retries the creation POST. A timeout therefore isn't a reason to submit a new task immediately.
- For cancellation or a deadline, use
errors.Isto identify the context error. Extract*solvethree.Errorwitherrors.Asand retain itsTaskIDandRequestID. - If you have a task ID, retrieve it with
GetTask. Otherwise, recover by replaying the exact original task inputs with the same request ID. Calling again without that ID creates a new logical request; changing inputs while reusing it conflicts. - For
*TaskFailedError, inspectErrorCodeand retrieve the failed task for its error text. That task is terminal: stop polling it and fix the cause rather than retrying unchanged inputs. - For an HTTP API error, extract the nested
*APIErrorto inspectStatusCodeandRetryAfter. Don't confuse an HTTP failure with a terminal failed task.
Sources and API reference
Run your first task.
Start with $5 in workspace credit.