Tutorials

Compensating Transactions for Failed CAPTCHA Workflows

A multi-step workflow creates an account (step 1), solves a CAPTCHA (step 2), submits a form (step 3), and verifies email (step 4). Step 3 fails with a server error. Now you have a solved CAPTCHA token you paid for, a half-created account, and no form submission. Compensating transactions undo completed steps in reverse order to keep your system consistent.

When You Need Compensation

Unlike database transactions, CAPTCHA solves can't be rolled back — the API already processed the task. Compensation means undoing the effects of prior steps:

Step Action Compensation
Create account POST to registration endpoint Delete or deactivate account
Solve CAPTCHA Submit to CaptchaAI Report incorrect (reclaim credit)
Submit form POST form data No-op or flag for manual cleanup
Store result Insert into database Delete the record

Python: Saga with Compensation

import requests
import time
from dataclasses import dataclass, field

API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"


@dataclass
class StepResult:
    name: str
    data: dict = field(default_factory=dict)
    compensated: bool = False


class Saga:
    """Execute a sequence of steps with automatic compensation on failure."""

    def __init__(self):
        self._steps: list[tuple[str, callable, callable]] = []
        self._completed: list[StepResult] = []

    def add_step(self, name: str, execute: callable, compensate: callable):
        """Add a step with its compensation action."""
        self._steps.append((name, execute, compensate))

    def run(self, context: dict) -> dict:
        """Execute all steps. On failure, compensate in reverse order."""
        for name, execute, compensate in self._steps:
            try:
                result = execute(context)
                step_result = StepResult(name=name, data=result or {})
                self._completed.append(step_result)
                context[name] = result or {}
                print(f"[SAGA] ✓ {name}")
            except Exception as e:
                print(f"[SAGA] ✗ {name} failed: {e}")
                self._compensate(context)
                raise SagaFailedError(
                    f"Step '{name}' failed: {e}",
                    completed=[s.name for s in self._completed],
                    compensated=[s.name for s in self._completed if s.compensated],
                ) from e

        return context

    def _compensate(self, context: dict):
        """Run compensation actions in reverse order."""
        for name, _, compensate in reversed(self._steps):
            matching = [s for s in self._completed if s.name == name]
            if not matching:
                continue

            try:
                compensate(context)
                matching[0].compensated = True
                print(f"[SAGA] ↩ Compensated: {name}")
            except Exception as e:
                print(f"[SAGA] ⚠ Compensation failed for {name}: {e}")


class SagaFailedError(Exception):
    def __init__(self, message: str, completed: list[str], compensated: list[str]):
        super().__init__(message)
        self.completed = completed
        self.compensated = compensated


# --- Step implementations ---

def solve_captcha(context: dict) -> dict:
    """Step: Solve the CAPTCHA via CaptchaAI."""
    params = {
        "key": API_KEY, "json": 1,
        "method": "turnstile",
        "sitekey": context["sitekey"],
        "pageurl": context["pageurl"],
    }
    resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
    if resp.get("status") != 1:
        raise RuntimeError(f"Submit failed: {resp.get('request')}")

    task_id = resp["request"]
    start = time.monotonic()
    while time.monotonic() - start < 180:
        time.sleep(5)
        poll = requests.get(RESULT_URL, params={
            "key": API_KEY, "action": "get", "id": task_id, "json": 1
        }, timeout=15).json()

        if poll.get("request") == "CAPCHA_NOT_READY":
            continue
        if poll.get("status") == 1:
            return {"token": poll["request"], "task_id": task_id}
        raise RuntimeError(f"Solve failed: {poll.get('request')}")

    raise RuntimeError("Timeout")


def compensate_captcha(context: dict):
    """Compensation: Report the task as incorrect to reclaim credit."""
    task_id = context.get("solve_captcha", {}).get("task_id")
    if task_id:
        requests.get(RESULT_URL, params={
            "key": API_KEY, "action": "reportbad", "id": task_id, "json": 1,
        }, timeout=15)


def submit_form(context: dict) -> dict:
    """Step: Submit the form with the CAPTCHA token."""
    token = context["solve_captcha"]["token"]
    resp = requests.post(context["submit_url"], data={
        "captcha_token": token,
        "email": context["email"],
        "username": context["username"],
    }, timeout=30)

    if resp.status_code != 200:
        raise RuntimeError(f"Form submit failed: {resp.status_code}")

    return {"confirmation_id": resp.json().get("id")}


def compensate_form(context: dict):
    """Compensation: Cancel the submission if possible."""
    conf_id = context.get("submit_form", {}).get("confirmation_id")
    if conf_id:
        requests.delete(
            f"{context['submit_url']}/{conf_id}",
            timeout=15,
        )


def store_result(context: dict) -> dict:
    """Step: Store the result in the database."""
    # Simulated database insert
    record = {
        "email": context["email"],
        "confirmation_id": context["submit_form"]["confirmation_id"],
        "captcha_task_id": context["solve_captcha"]["task_id"],
    }
    print(f"[DB] Stored: {record}")
    return {"record_id": "db-123"}


def compensate_store(context: dict):
    """Compensation: Delete the database record."""
    record_id = context.get("store_result", {}).get("record_id")
    if record_id:
        print(f"[DB] Deleted record: {record_id}")


# --- Build and run the saga ---

saga = Saga()
saga.add_step("solve_captcha", solve_captcha, compensate_captcha)
saga.add_step("submit_form", submit_form, compensate_form)
saga.add_step("store_result", store_result, compensate_store)

context = {
    "sitekey": "0x4XXXXXXXXXXXXXXXXX",
    "pageurl": "https://example.com/register",
    "submit_url": "https://example.com/api/register",
    "email": "user@example.com",
    "username": "testuser",
}

try:
    result = saga.run(context)
    print(f"Saga completed: {result.get('store_result')}")
except SagaFailedError as e:
    print(f"Saga failed: {e}")
    print(f"  Completed steps: {e.completed}")
    print(f"  Compensated steps: {e.compensated}")

JavaScript: Async Saga

const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

class Saga {
  #steps = [];
  #completed = [];

  addStep(name, execute, compensate) {
    this.#steps.push({ name, execute, compensate });
    return this;
  }

  async run(context) {
    for (const step of this.#steps) {
      try {
        const result = await step.execute(context);
        this.#completed.push(step);
        context[step.name] = result || {};
        console.log(`[SAGA] ✓ ${step.name}`);
      } catch (error) {
        console.log(`[SAGA] ✗ ${step.name}: ${error.message}`);
        await this.#compensate(context);
        throw error;
      }
    }
    return context;
  }

  async #compensate(context) {
    for (const step of [...this.#completed].reverse()) {
      try {
        await step.compensate(context);
        console.log(`[SAGA] ↩ ${step.name}`);
      } catch (e) {
        console.warn(`[SAGA] Compensation failed: ${step.name}: ${e.message}`);
      }
    }
  }
}

async function solveCaptcha(ctx) {
  const body = new URLSearchParams({
    key: API_KEY, json: "1", method: "turnstile",
    sitekey: ctx.sitekey, pageurl: ctx.pageurl,
  });
  const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
  if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);

  const taskId = resp.request;
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
    const poll = await (await fetch(url)).json();
    if (poll.request === "CAPCHA_NOT_READY") continue;
    if (poll.status === 1) return { token: poll.request, taskId };
    throw new Error(`Solve: ${poll.request}`);
  }
  throw new Error("Timeout");
}

async function compensateCaptcha(ctx) {
  const taskId = ctx.solveCaptcha?.taskId;
  if (taskId) {
    await fetch(`${RESULT_URL}?key=${API_KEY}&action=reportbad&id=${taskId}&json=1`);
  }
}

async function submitForm(ctx) {
  const resp = await fetch(ctx.submitUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      captcha_token: ctx.solveCaptcha.token,
      email: ctx.email,
    }),
  });
  if (!resp.ok) throw new Error(`Form: ${resp.status}`);
  return await resp.json();
}

async function compensateForm(ctx) {
  const id = ctx.submitForm?.id;
  if (id) await fetch(`${ctx.submitUrl}/${id}`, { method: "DELETE" });
}

// Build and run
const saga = new Saga()
  .addStep("solveCaptcha", solveCaptcha, compensateCaptcha)
  .addStep("submitForm", submitForm, compensateForm);

try {
  const result = await saga.run({
    sitekey: "0x4XXXXXXXXXXXXXXXXX",
    pageurl: "https://example.com/register",
    submitUrl: "https://example.com/api/register",
    email: "user@example.com",
  });
  console.log("Success:", result.submitForm);
} catch (error) {
  console.error("Saga failed:", error.message);
}

Compensation Strategies

Strategy Use when Example
Report bad CAPTCHA token was unused Call reportbad to reclaim credit
Delete record Database row was created DELETE the row
Status update Can't delete but can invalidate Mark record as cancelled
Notification Manual intervention needed Send alert for failed compensation
No-op Step has no reversible effect Skip compensation (logging, metrics)

Troubleshooting

Issue Cause Fix
Compensation itself fails Target service down Log for manual cleanup; don't retry infinitely
reportbad doesn't refund Task was already accepted as correct Report before the token is used
Partial compensation Middle step's compensation threw Handle each compensation independently — don't stop on first error
Context missing required data Step didn't store identifiers Always return IDs and references from each step
Compensation runs twice Saga retried after first compensation Use idempotent compensation actions

FAQ

Should I always report CAPTCHA tasks as bad during compensation?

Only report if the token wasn't used. If the form submission failed for a reason unrelated to the CAPTCHA (server error, validation failure), the solve was technically correct. Reporting correct solves as bad can affect your account reputation.

How do I handle compensation failures?

Log the failure and continue compensating remaining steps. After the saga completes, surface uncompensated steps for manual review. Never let a compensation failure prevent other compensations from running.

Can I retry the saga instead of compensating?

Yes, for transient failures. Add retry logic before triggering compensation. Only compensate when the error is permanent or retries are exhausted. Combine with idempotency so retried steps don't create duplicates.

Next Steps

Build resilient multi-step CAPTCHA workflows — get your CaptchaAI API key and implement the saga pattern.

Related guides:

Full Working Code

Complete runnable examples for this article in Python, Node.js, PHP, Go, Java, C#, Ruby, Rust, Kotlin & Bash.

View on GitHub →

Discussions (0)

No comments yet.