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:
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.