Use the Go SDK in a Gin handler

By solvethree4 min read

Use one shared solvethree client and pass a deadline-bound child of c.Request.Context() to Solve. Keep the target configuration and API key on the server, and authenticate the application caller before creating a task.

On this page

Install the dependencies and configure the target

  1. 1.

    Create the Go module and install dependencies

    Use Go 1.25 or later for the current Gin release. The solvethree SDK itself requires Go 1.22+. Initialize your application's module with go mod init, using your own module path. Install Gin following its quickstart, then use go get to add solvethree.com/[email protected]. The official SDK is served through the domain-hosted public module proxy; it is served from solvethree.com.

  2. 2.

    Set the server credentials

    Set SOLVETHREE_API_KEY to your workspace API key and INTEGRATION_KEY to the secret your internal application will send in X-Integration-Key. These are separate credentials: the application caller doesn't need the solvethree API key.

  3. 3.

    Configure the fixed target

    Set CAPTCHA_SITE to the target hostname and SOLVETHREE_PROXY to the proxy used for solving. Create an active site profile in the workspace first; see site profile setup. The example fixes Action to login, so change that value if your integration uses another action. Setting the hostname and action doesn't create a profile.

Add the authenticated Gin route

The client is created once and is safe for concurrent requests. The route checks the internal application credential before calling solvethree. It accepts a request ID from the caller, but not an arbitrary target or proxy.

Gin · bounded solving with the official Go SDK
package main

import (
    "context"
    "errors"
    "log"
    "net/http"
    "os"
    "time"
    "github.com/gin-gonic/gin"
    solvethree "solvethree.com/sdk"
)

func main() {
    client,err:=solvethree.NewClient(os.Getenv("SOLVETHREE_API_KEY"),nil)
    if err!=nil { log.Fatal(err) }
    secret:=os.Getenv("INTEGRATION_KEY")
    if secret=="" { log.Fatal("Set INTEGRATION_KEY") }
    router:=gin.New()
    router.Use(gin.Recovery())
    router.POST("/captcha/solve",func(c *gin.Context){
        if c.GetHeader("X-Integration-Key")!=secret { c.Status(http.StatusUnauthorized);return }
        input:=solvethree.TaskRequest{Site:os.Getenv("CAPTCHA_SITE"),Action:"login",
            Proxy:os.Getenv("SOLVETHREE_PROXY"),RequestID:c.GetHeader("X-Request-ID")}
        ctx,cancel:=context.WithTimeout(c.Request.Context(),180*time.Second);defer cancel()
        result,err:=client.Solve(ctx,input,solvethree.SolveOptions{})
        if err!=nil {
            var failed *solvethree.TaskFailedError
            var recovery *solvethree.Error
            if errors.As(err,&failed) { c.JSON(502,gin.H{"taskId":failed.TaskID,"errorCode":failed.ErrorCode});return }
            if errors.As(err,&recovery) { c.JSON(502,gin.H{"requestId":recovery.RequestID,"taskId":recovery.TaskID});return }
            c.JSON(400,gin.H{"error":"Invalid task input"});return
        }
        // Return private solution data only to the authorized application caller.
        c.JSON(200,result)
    })
    log.Fatal(router.Run("127.0.0.1:3000"))
}

The task supplies Site, Action, and a single Proxy. It leaves Type and URL unset, as required for reCAPTCHA, and uses the default Enterprise v3 mode.

The explicit context deadline bounds the handler's wait to 180 seconds; client disconnection can cancel it sooner. The HTTP server and any reverse proxy must permit that request duration. If they can't, use separate create/retrieve operations rather than keeping this synchronous route open.

The listener binds to loopback. Integrate the route with your application's existing internal authentication and deployment setup; don't expose a paid solve endpoint without caller authentication.

Handle errors without exposing secrets

The handler uses errors.As rather than matching error strings. A terminal TaskFailedError produces a 502 response containing the task ID and error code. Other wrapped SDK errors produce a 502 containing recovery IDs. The fallback returns a fixed 400 message instead of exposing raw errors, credentials, or proxy URLs.

Those 502 responses don't all mean the solve failed. Use errors.Is to distinguish cancellation or deadline expiry. For HTTP-specific handling, errors.As can extract a nested APIError with StatusCode and RetryAfter. Keep that distinction in internal diagnostics rather than treating every error as a reason to create another task.

A terminal failed task should not be polled again. Retrieve it if you need its error text, then correct the cause before starting another solve. Failed solves are refunded.

Keep the IDs when the request times out

Cancellation stops waiting, not remote work. The example returns recovery IDs when it can, but it doesn't persist them. If the caller disconnects, that JSON response may never arrive.

  1. 1.

    Assign the request ID before calling the route

    Have the internal caller retain a stable X-Request-ID for each logical solve. Valid IDs contain 1–64 ASCII letters, numbers, underscores, or hyphens. The SDK generates an ID when one is missing, but assigning it beforehand makes recovery possible even if the response is lost.

  2. 2.

    Retain the exact task inputs

    Store the hostname, action, proxy, and request ID in protected application storage. Retrying after an environment configuration change must not silently substitute different task inputs.

  3. 3.

    Recover before creating new work

    With a known task ID, call GetTask using a fresh bounded context. Without one, replay the original task through CreateTask with the same request ID and inputs. That returns the existing task; different inputs with the same ID conflict. Continue retrieving only while the task is pending or solving.

Solve creates once and retries only temporary polling responses with HTTP status 429, 502, 503, or 504, honoring Retry-After within the context deadline. CreateTask never retries POST automatically, so a creation timeout still needs recovery rather than a new request ID.

Submit the token with its returned identity

A successful handler response contains private solution data for the authorized application caller. Use Solution.Token, not the task ID, in the target request. Use the returned UserAgent and SecChUA, and obtain the successful proxy with SuccessfulProxy(originalTask, readyResult).

The SDK doesn't submit anything to the target. A ready task means a solution was generated, not that the website accepted it. Check the target's response separately.

Run your first task.

Start with $5 in workspace credit.

Create a workspace