Solve

Detect and solve browser challenges (Cloudflare Turnstile, CAPTCHAs, interstitials, etc.) on the current page.

PinchTab ships with a pluggable solver framework. Each solver targets a specific provider (e.g. Cloudflare). Solvers are registered at startup and can be invoked explicitly by name or discovered automatically.

Endpoints

GET  /solvers
POST /solve
POST /solve/{name}
POST /tabs/{id}/solve
POST /tabs/{id}/solve/{name}

List Solvers

terminal
curl http://localhost:9867/solvers
curl http://localhost:9867/solvers
{
  "solvers": ["cloudflare"]
}

Auto-Detect Solve

When no solver field is provided, PinchTab tries each registered solver in order. The first one whose CanHandle returns true is used.

terminal
curl -X POST http://localhost:9867/solve \  -H "Content-Type: application/json" \  -d '{"maxAttempts": 3, "timeout": 30000}'
curl -X POST http://localhost:9867/solve \  -H "Content-Type: application/json" \  -d '{"maxAttempts": 3, "timeout": 30000}'

If no challenge is detected on the page, the response returns immediately with solved: true and attempts: 0.

Named Solver

Specify the solver by name in the body or path:

terminal
# Bodycurl -X POST http://localhost:9867/solve \  -H "Content-Type: application/json" \  -d '{"solver": "cloudflare", "maxAttempts": 3}'# Pathcurl -X POST http://localhost:9867/solve/cloudflare \  -H "Content-Type: application/json" \  -d '{"maxAttempts": 3}'
# Bodycurl -X POST http://localhost:9867/solve \  -H "Content-Type: application/json" \  -d '{"solver": "cloudflare", "maxAttempts": 3}'# Pathcurl -X POST http://localhost:9867/solve/cloudflare \  -H "Content-Type: application/json" \  -d '{"maxAttempts": 3}'

Tab-Scoped Solve

terminal
curl -X POST http://localhost:9867/tabs/{tabId}/solve \  -H "Content-Type: application/json" \  -d '{"solver": "cloudflare"}'
curl -X POST http://localhost:9867/tabs/{tabId}/solve \  -H "Content-Type: application/json" \  -d '{"solver": "cloudflare"}'

Request Body

FieldTypeDefaultDescription
tabIdstringTab ID (optional, uses default tab)
solverstringSolver name (optional, auto-detect)
maxAttemptsint3Maximum solve attempts
timeoutfloat30000Overall timeout in milliseconds

Response

{
  "tabId": "DEADBEEF",
  "solver": "cloudflare",
  "solved": true,
  "challengeType": "managed",
  "attempts": 1,
  "title": "thuisbezorgd.nl"
}
FieldTypeDescription
tabIdstringTab the solve ran on
solverstringWhich solver handled the challenge
solvedboolWhether the challenge was resolved
challengeTypestringChallenge variant (e.g. managed, embedded)
attemptsintNumber of attempts made
titlestringFinal page title

Error Responses

CodeMeaning
400Invalid body or unknown solver name
404Tab not found
423Tab locked by another owner
500CDP/Chrome error

Built-In Solvers

Cloudflare (cloudflare)

Handles Cloudflare Turnstile and interstitial challenges.

Detection: Checks the page title for known Cloudflare indicators (“Just a moment…”, “Attention Required”, “Checking your browser”).

Challenge types:

TypeHandling
non-interactiveWaits for auto-resolution (up to 15s)
managedLocates Turnstile iframe, clicks checkbox
interactiveSame as managed
embeddedDetects via Turnstile script tag, clicks checkbox

Click strategy: The solver uses human-like mouse input (Bezier curve movement, random delays, press/release offset) to click the Turnstile checkbox. Click coordinates are computed relative to the widget dimensions (not hardcoded pixel offsets) with randomised jitter.

Stealth requirement: The Cloudflare solver works best with stealthLevel: "full" in the PinchTab config. Cloudflare evaluates browser fingerprints (CDP detection, WebGL, canvas, navigator properties) before and after the checkbox interaction. Without full stealth, the solver may click correctly but the challenge can still fail fingerprint verification. Check stealth status with GET /stealth/status.

Writing a Custom Solver

Implement the solver.Solver interface and register it during init():

package mygateway

import (
    "context"
    "github.com/pinchtab/pinchtab/internal/solver"
)

func init() {
    solver.MustRegister("mygateway", &MyGatewaySolver{})
}

type MyGatewaySolver struct{}

func (s *MyGatewaySolver) Name() string { return "mygateway" }

func (s *MyGatewaySolver) CanHandle(ctx context.Context) (bool, error) {
    // Check page markers (title, DOM elements, etc.)
    return false, nil
}

func (s *MyGatewaySolver) Solve(ctx context.Context, opts solver.Options) (*solver.Result, error) {
    // Detect, interact, and resolve the challenge.
    return &solver.Result{Solver: "mygateway", Solved: true}, nil
}

The solver has access to the full chromedp context for CDP operations.