Setup
Before you start
- Go 1.18 or newer
- Nothing to install: only
net/http,encoding/jsonandcontextfrom the standard library - A bypass.so API key (create one) with a positive balance
Install
mkdir bypass-demo && cd bypass-demo
go mod init bypass-demoStore your key
The examples read your key from the BYPASS_API_KEY environment variable, so it never lands in source control.
export BYPASS_API_KEY="your-api-key"Check that the key works: curl -H "Authorization: Bearer $BYPASS_API_KEY" https://bypass.so/balance returns your balance.
Step 1
Add the helper
Save this file next to your code. It wraps the whole task lifecycle in one call, Solve(ctx, taskType, params), so the rest of your project never deals with polling.
- createTask: sends your key, the task type and its fields as one flat JSON object.
- Fast path: if the answer is already ready, it returns at once.
- Polling: otherwise it checks
/getTaskResult/{taskId}every two seconds until the result is ready or the timeout runs out. - Errors: any reply with a non-zero
errorId, including afailedtask, becomes an error carrying theerrorCode, so you can branch on it. - taskId: kept with the solution, so you can report a wrong answer later.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
const apiURL = "https://bypass.so"
var httpClient = &http.Client{Timeout: 30 * time.Second}
// BypassError is returned for every API reply with a non-zero errorId.
type BypassError struct {
Code string
Description string
TaskID string
}
func (e *BypassError) Error() string { return e.Code + ": " + e.Description }
// Solution holds the answer. Text is set for recognition types, Token for widget types.
type Solution struct {
Text string `json:"text,omitempty"`
Token string `json:"token,omitempty"`
GRecaptchaResponse string `json:"gRecaptchaResponse,omitempty"`
TokenField string `json:"tokenField,omitempty"`
TokenLifetime float64 `json:"tokenLifetime,omitempty"`
TaskID string `json:"-"`
}
// Answer returns the token or text, whichever this task type produces.
func (s *Solution) Answer() string {
switch {
case s.GRecaptchaResponse != "":
return s.GRecaptchaResponse
case s.Token != "":
return s.Token
default:
return s.Text
}
}
type apiError struct {
ErrorID int `json:"errorId"`
ErrorCode string `json:"errorCode"`
ErrorDescription string `json:"errorDescription"`
TaskID string `json:"taskId"`
}
type taskReply struct {
TaskID string `json:"taskId"`
Status string `json:"status"`
Solution Solution `json:"solution"`
}
func apiKey() string { return os.Getenv("BYPASS_API_KEY") }
// call sends one request and decodes the JSON reply into out, turning errorId != 0 into a *BypassError.
func call(ctx context.Context, method, path string, body any, bearer bool, out any) error {
var payload io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return err
}
payload = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, apiURL+path, payload)
if err != nil {
return err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if bearer {
req.Header.Set("Authorization", "Bearer "+apiKey())
}
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var head apiError
if err := json.Unmarshal(raw, &head); err != nil {
return &BypassError{Code: "HTTP_ERROR", Description: fmt.Sprintf("HTTP %d without a JSON body", resp.StatusCode)}
}
if head.ErrorID != 0 {
if head.ErrorCode == "" {
head.ErrorCode = "ERROR_UNKNOWN"
}
return &BypassError{Code: head.ErrorCode, Description: head.ErrorDescription, TaskID: head.TaskID}
}
return json.Unmarshal(raw, out)
}
// Solve creates a task and polls every 2 seconds until it is ready.
// Without a deadline on ctx it gives up after 120 seconds.
func Solve(ctx context.Context, taskType string, params map[string]any) (*Solution, error) {
key := apiKey()
if key == "" {
return nil, &BypassError{Code: "ERROR_KEY_INVALID", Description: "set the BYPASS_API_KEY environment variable"}
}
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 120*time.Second)
defer cancel()
}
body := map[string]any{"clientKey": key, "type": taskType}
for k, v := range params {
body[k] = v
}
var r taskReply
if err := call(ctx, http.MethodPost, "/createTask", body, false, &r); err != nil {
return nil, err
}
taskID := r.TaskID
poll := "/getTaskResult/" + url.PathEscape(taskID) + "?clientKey=" + url.QueryEscape(key)
for r.Status != "ready" {
if r.Status == "failed" {
return nil, &BypassError{Code: "ERROR_CAPTCHA_UNSOLVABLE", Description: "task failed", TaskID: taskID}
}
select {
case <-ctx.Done():
return nil, fmt.Errorf("task %s was not ready in time: %w", taskID, ctx.Err())
case <-time.After(2 * time.Second):
}
r = taskReply{}
if err := call(ctx, http.MethodGet, poll, nil, false, &r); err != nil {
return nil, err
}
}
sol := r.Solution
sol.TaskID = taskID
return &sol, nil
}
type Balance struct {
Credits int64 `json:"credits"`
BalanceUSD float64 `json:"balanceUsd"`
}
// GetBalance returns the balance of the key in BYPASS_API_KEY.
func GetBalance(ctx context.Context) (*Balance, error) {
var b Balance
err := call(ctx, http.MethodGet, "/balance", nil, true, &b)
return &b, err
}
type Report struct {
TaskID string `json:"taskId"`
Refunded int64 `json:"refunded"`
BalanceUSD float64 `json:"balanceUsd"`
}
// ReportBad flags a wrong answer. The price is refunded once and the task is closed.
func ReportBad(ctx context.Context, taskID string) (*Report, error) {
var r Report
err := call(ctx, http.MethodPost, "/reportBad?task_id="+url.QueryEscape(taskID), nil, true, &r)
return &r, err
}Good to know
Solve takes a context.Context. Without a deadline it stops after 120 seconds; pass context.WithTimeout to choose your own, or cancel the context to stop polling early.Project layout used below:
bypass-demo/ go.mod bypass.go the helper (step 1) main.go your program (step 2)
Step 2
Solve any supported type
Every task type uses the same call. Only the type name and its fields change. Pick a tab to see a complete, runnable file, then run it with go run .. Tabs marked soon are ready for launch day, but those types are not live yet.
package main
import (
"context"
"fmt"
"log"
)
func main() {
sol, err := Solve(context.Background(), "MathCaptchaTask", map[string]any{
"captchaText": "12 + 30 = ?",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(sol.Text)
}go run .reCAPTCHA v3 and hCaptcha are coming soon
RecaptchaV3Task and HCaptchaTask are not live yet. Until launch, createTask returns ERROR_TASK_NOT_SUPPORTED and nothing is charged. Get notified when they ship.Token types and your form
g-recaptcha-response, h-captcha-response or cf-turnstile-response. Send the exact URL of the page that shows the widget as websiteURL.Step 3
Handle errors and retries
Some failures are worth a second try and some are not. A missing balance or a wrong key will fail again, so stop and fix it. An unsolvable task, a timeout or a network hiccup is worth one or two more attempts with a short pause. errors.As separates API errors (a *BypassError with a Code) from network errors and expired contexts, so each one gets the right treatment.
package main
import (
"context"
"errors"
"fmt"
"log"
"net"
"time"
)
var retryable = map[string]bool{
"ERROR_CAPTCHA_UNSOLVABLE": true,
"ERROR_INTERNAL": true,
"HTTP_ERROR": true,
}
// SolveWithRetry retries failures that can succeed on a second try and stops on the rest.
func SolveWithRetry(taskType string, params map[string]any, attempts int) (*Solution, error) {
for attempt := 1; ; attempt++ {
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
sol, err := Solve(ctx, taskType, params)
cancel()
if err == nil {
return sol, nil
}
var apiErr *BypassError
var netErr net.Error
retry := errors.Is(err, context.DeadlineExceeded) || errors.As(err, &netErr)
if errors.As(err, &apiErr) {
retry = retryable[apiErr.Code]
}
if !retry || attempt >= attempts {
return nil, err
}
log.Printf("attempt %d failed (%v), retrying", attempt, err)
time.Sleep(time.Duration(2*attempt) * time.Second)
}
}
func main() {
sol, err := SolveWithRetry("MathCaptchaTask", map[string]any{"captchaText": "12 + 30 = ?"}, 3)
if err != nil {
log.Fatal(err)
}
fmt.Println(sol.Text)
}Failed tasks cost nothing
Step 4
Check your balance and flag wrong answers
GET /balance and POST /reportBad use your key as a Bearer token. Report a task only when the target site actually rejected its answer. The refund is applied once per task, and the task is closed afterwards.
package main
import (
"context"
"fmt"
"log"
)
func flagWrongAnswer(ctx context.Context, sol *Solution) error {
r, err := ReportBad(ctx, sol.TaskID)
if err != nil {
return err
}
fmt.Println("refunded units:", r.Refunded, "balance now:", r.BalanceUSD)
return nil
}
func main() {
b, err := GetBalance(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("balance: $%.2f\n", b.BalanceUSD)
}Scale
Solve many tasks in parallel
Each task is independent, so throughput grows with concurrency. One goroutine per file, with a buffered channel as a semaphore so that at most eight tasks are in flight. Solve is safe to call from many goroutines at once. Start with a limit of 8 and adjust while you watch error rates and spend.
package main
import (
"context"
"encoding/base64"
"fmt"
"log"
"os"
"path/filepath"
"sync"
)
func main() {
files, err := filepath.Glob("captchas/*.png")
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
limit := make(chan struct{}, 8) // at most 8 tasks in flight
var wg sync.WaitGroup
for _, path := range files {
wg.Add(1)
go func(path string) {
defer wg.Done()
limit <- struct{}{}
defer func() { <-limit }()
img, err := os.ReadFile(path)
if err != nil {
log.Println(path, err)
return
}
sol, err := Solve(ctx, "ImageToTextTask", map[string]any{
"imageBase64": base64.StdEncoding.EncodeToString(img),
})
if err != nil {
log.Println(path, "failed:", err)
return
}
fmt.Printf("%s: %s\n", filepath.Base(path), sol.Text)
}(path)
}
wg.Wait()
}Reference
Task type reference
What each type expects and returns. Prices are per 1,000 solved tasks; larger top-ups add bonus credit (see pricing).
| Task type | You send | You get | Price / 1k |
|---|---|---|---|
Task typeMathCaptchaTask | You sendcaptchaText | You gettext | Price / 1k$0.35 |
Task typeImageToTextTask | You sendimageBase64 | You gettext | Price / 1k$0.28 |
Task typeAudioCaptchaTask | You sendaudioBase64 | You gettext | Price / 1k$0.35 |
Task typeRecaptchaV2Task | You sendwebsiteURLwebsiteKey | You getgRecaptchaResponse and token, plus tokenField | Price / 1k$0.56 |
Task typeTurnstileTask | You sendwebsiteURLwebsiteKey | You gettoken, plus tokenField | Price / 1k$0.84 |
Task typeRecaptchaV3TaskComing soon | You sendwebsiteURLwebsiteKeyaction | You getgRecaptchaResponse and token, plus tokenField | Price / 1kAt launch |
Task typeHCaptchaTaskComing soon | You sendwebsiteURLwebsiteKey | You gettoken, plus tokenField | Price / 1kAt launch |
Errors
Troubleshooting
| Error | Source | What it means | How to fix it |
|---|---|---|---|
ErrorERROR_KEY_INVALID | SourceHTTP 403 | What it meansThe key is missing, mistyped or revoked. | How to fix itCheck that BYPASS_API_KEY is set in the same shell that runs your code, with no quotes or spaces around it. |
ErrorERROR_INSUFFICIENT_CREDITS | SourceHTTP 402 | What it meansYour balance cannot cover the task price. | How to fix itTop up from the dashboard. Nothing was charged for this request. |
ErrorERROR_TASK_NOT_SUPPORTED | SourceHTTP 200 | What it meansThe type string is unknown, or the type is still coming soon (reCAPTCHA v3, hCaptcha). | How to fix itCopy the exact name, for example TurnstileTask. Names are case-sensitive. Nothing is charged. |
ErrorERROR_BAD_REQUEST | SourceHTTP 400 | What it meansThe body is not valid JSON, or a field has the wrong type. | How to fix itSend Content-Type: application/json and string values for clientKey, type and the task fields. |
ErrorERROR_CAPTCHA_UNSOLVABLE | SourceHTTP 200 | What it meansThis task could not be completed. | How to fix itYou are not charged. Check the image, sitekey and page URL, then submit a new task. |
ErrorERROR_NOT_FOUND | SourceHTTP 404 | What it meansThe taskId is unknown or belongs to another key. | How to fix itPoll with the taskId returned by your own createTask call, using the same key. |
ErrorERROR_INTERNAL | SourceHTTP 500 | What it meansSomething went wrong on our side. | How to fix itRetry after a short pause. The helper’s retry wrapper does this for you. |
| ErrorHelper timeout | Sourceyour code | What it meansNo result inside your timeout. | How to fix itToken types usually need several seconds. Keep at least 60–120 s and retry with a new task. |
ErrorHTTP_ERROR | Sourceyour code | What it meansA proxy or network layer answered without JSON. | How to fix itRetry with backoff. If it persists, check that your code calls https://bypass.so. |
Every error body has the same shape: {"errorId":1,"errorCode":"…","errorDescription":"…"}. Full list in the API reference.
Production
Best practices
Keep the key out of code
Read it from an environment variable or your secret store. Never commit it or ship it to a web page.
Solve right before submit
Tokens are short-lived. Fill the form first, then request the token and submit at once.
Cap your concurrency
Start with 5–10 parallel tasks and raise it while you watch error rates and spend.
Log every taskId
It is the only thing you need to debug a result or report a wrong answer.
Match the page exactly
Send the URL where the widget renders and the sitekey from that page, not from a different form.
Report wrong answers
If a site rejects a solved result, call reportBad with its taskId. The price returns to your balance once.
FAQ
Questions
"status": "ready" and a solution, so the helper returns without polling. Token types always come back as processing first.reportBad with its taskId and the price returns to your balance once. A reported task is closed, so submit a new one to try again.httpClient with a 30-second timeout. Replace it with a client that has your proxy, transport or tracing settings; the rest of the code stays the same.Policy
Responsible use
Use it where you are allowed to
Automate pages you own, test environments you run, or sites whose terms permit automated access. Do not use bypass.so for fraud, account takeover, spam, scalping, evading bans or collecting personal data without a lawful basis. Keep request rates reasonable and stop when a site asks you to. Our acceptable use policy applies to every key, and keys used for abuse are closed.
Keep going
Next steps
- API referenceEvery endpoint, field and error code
- PricingPer-task prices and top-up bonuses
- Python guideScripts, backends and data jobs
- Node.js guideServices, CLIs and serverless functions
- PHP guideWeb apps, cron jobs and CLI scripts
- Java guideJVM backends on Java 17+, no libraries
- C# / .NET guideAsync .NET 8 services and tools
- Ruby guideScripts, Rails apps and background jobs
- Selenium guideQA suites and WebDriver flows
- Playwright guideEnd-to-end tests in Node.js or Python
- Puppeteer guideChrome automation scripts
- Scrapy guideCrawls that run into captcha pages