When you use CaptchaAI's callback URL feature (pingback), your server exposes an HTTP endpoint that receives CAPTCHA solutions. Without validation, anyone who discovers that URL can send fake solutions. This tutorial covers how to secure callback endpoints.
The Callback Flow
1. You submit task:
POST https://ocr.captchaai.com/in.php
?key=YOUR_API_KEY
&method=userrecaptcha
&googlekey=SITE_KEY
&pageurl=https://example.com
&pingback=https://your-server.com/captcha/callback
2. CaptchaAI solves the CAPTCHA
3. CaptchaAI sends result to your endpoint:
GET https://your-server.com/captcha/callback?id=TASK_ID&code=SOLUTION_TOKEN
The problem: step 3 is an unauthenticated request. You need to verify it actually came from CaptchaAI.
Validation Strategy 1: Task ID Verification
The simplest approach — only accept callback results for task IDs you actually submitted.
Python (Flask)
import os
import threading
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
# Thread-safe set of pending task IDs
pending_tasks = set()
pending_lock = threading.Lock()
results = {}
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
def submit_captcha(sitekey, pageurl):
"""Submit CAPTCHA and register the task ID."""
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"pingback": "https://your-server.com/captcha/callback",
"json": 1
})
data = resp.json()
if data.get("status") == 1:
task_id = data["request"]
with pending_lock:
pending_tasks.add(task_id)
return task_id
return None
@app.route("/captcha/callback")
def captcha_callback():
task_id = request.args.get("id")
solution = request.args.get("code")
# Validate: only accept known task IDs
with pending_lock:
if task_id not in pending_tasks:
return jsonify({"error": "unknown task"}), 403
pending_tasks.discard(task_id)
results[task_id] = solution
return "OK", 200
JavaScript (Express)
const express = require("express");
const axios = require("axios");
const app = express();
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const pendingTasks = new Set();
const results = new Map();
async function submitCaptcha(sitekey, pageurl) {
const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
pingback: "https://your-server.com/captcha/callback",
json: 1,
},
});
if (resp.data.status === 1) {
const taskId = resp.data.request;
pendingTasks.add(taskId);
return taskId;
}
return null;
}
app.get("/captcha/callback", (req, res) => {
const taskId = req.query.id;
const solution = req.query.code;
// Validate: only accept known task IDs
if (!pendingTasks.has(taskId)) {
return res.status(403).json({ error: "unknown task" });
}
pendingTasks.delete(taskId);
results.set(taskId, solution);
res.sendStatus(200);
});
app.listen(3000);
Validation Strategy 2: HMAC Signature Token
Add a secret token to your callback URL that attackers can't guess.
Python
import hashlib
import hmac
import os
CALLBACK_SECRET = os.environ["CALLBACK_SECRET"] # Random 32+ character string
def generate_callback_url(task_id):
"""Generate callback URL with HMAC signature."""
signature = hmac.new(
CALLBACK_SECRET.encode(),
task_id.encode(),
hashlib.sha256
).hexdigest()
return f"https://your-server.com/captcha/callback?token={signature}"
@app.route("/captcha/callback")
def captcha_callback():
task_id = request.args.get("id")
token = request.args.get("token")
solution = request.args.get("code")
# Verify HMAC signature
expected = hmac.new(
CALLBACK_SECRET.encode(),
task_id.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(token, expected):
return jsonify({"error": "invalid signature"}), 403
results[task_id] = solution
return "OK", 200
JavaScript
const crypto = require("crypto");
const CALLBACK_SECRET = process.env.CALLBACK_SECRET;
function generateCallbackUrl(taskId) {
const signature = crypto
.createHmac("sha256", CALLBACK_SECRET)
.update(taskId)
.digest("hex");
return `https://your-server.com/captcha/callback?token=${signature}`;
}
app.get("/captcha/callback", (req, res) => {
const taskId = req.query.id;
const token = req.query.token;
const solution = req.query.code;
// Verify HMAC signature
const expected = crypto
.createHmac("sha256", CALLBACK_SECRET)
.update(taskId)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expected))) {
return res.status(403).json({ error: "invalid signature" });
}
results.set(taskId, solution);
res.sendStatus(200);
});
Use the generated URL when submitting: pingback=https://your-server.com/captcha/callback?token=HMAC_SIGNATURE.
Validation Strategy 3: IP Allowlisting
Restrict your callback endpoint to CaptchaAI's server IPs.
Python (Flask)
# CaptchaAI callback source IPs (verify current IPs with CaptchaAI support)
ALLOWED_IPS = {"138.201.XX.XX", "148.251.XX.XX"} # Replace with actual IPs
@app.before_request
def check_ip():
if request.path.startswith("/captcha/callback"):
client_ip = request.remote_addr
if client_ip not in ALLOWED_IPS:
return jsonify({"error": "forbidden"}), 403
JavaScript (Express)
const ALLOWED_IPS = new Set(["138.201.XX.XX", "148.251.XX.XX"]);
app.use("/captcha/callback", (req, res, next) => {
const clientIp = req.ip || req.connection.remoteAddress;
if (!ALLOWED_IPS.has(clientIp)) {
return res.status(403).json({ error: "forbidden" });
}
next();
});
Note: Contact CaptchaAI support for the current list of callback source IPs. If you're behind a reverse proxy, ensure
X-Forwarded-Forheaders are configured correctly.
Replay Attack Prevention
Even valid callbacks can be replayed. Add a timestamp check and one-time-use enforcement:
Python
import time
CALLBACK_TTL = 300 # Reject callbacks older than 5 minutes
used_callbacks = set()
@app.route("/captcha/callback")
def captcha_callback():
task_id = request.args.get("id")
timestamp = request.args.get("ts")
solution = request.args.get("code")
# Check timestamp freshness
if timestamp:
age = time.time() - float(timestamp)
if age > CALLBACK_TTL or age < 0:
return jsonify({"error": "expired"}), 403
# One-time use
if task_id in used_callbacks:
return jsonify({"error": "already processed"}), 409
used_callbacks.add(task_id)
results[task_id] = solution
return "OK", 200
Combined Security Checklist
| Layer | Protection Against | Implementation |
|---|---|---|
| Task ID verification | Random/unknown task injection | Store pending IDs, reject unknowns |
| HMAC signature | URL guessing, forged callbacks | Sign callback URL with secret |
| IP allowlisting | Requests from unauthorized servers | Whitelist CaptchaAI IPs |
| Replay prevention | Resubmitted valid callbacks | One-time-use + timestamp validation |
| HTTPS | Eavesdropping, man-in-the-middle | TLS on callback endpoint |
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| All callbacks rejected | IP allowlist doesn't include CaptchaAI IPs | Verify current IPs with support; check reverse proxy headers |
| HMAC verification fails | Task ID mismatch between submit and callback | Ensure you use the exact task ID returned by in.php |
| Duplicate callbacks processed | Race condition on concurrent callbacks | Use atomic set operations or database unique constraints |
| Callbacks timing out | Endpoint takes too long to respond | Process async — accept immediately, process in background |
FAQ
Should I use all four validation strategies together?
Use task ID verification (Strategy 1) as the minimum. Add HMAC signatures (Strategy 2) for public-facing endpoints. IP allowlisting (Strategy 3) is ideal if CaptchaAI publishes stable callback IPs. Replay prevention is essential for financial or sensitive workflows.
What happens if my callback endpoint is down when CaptchaAI sends the result?
The solution is still available via the polling endpoint (res.php). Implement a fallback that polls for any tasks that don't receive callbacks within a timeout period.
Can I use mutual TLS (mTLS) for callback authentication?
In theory, yes — but CaptchaAI's callback system uses standard HTTPS GET requests. HMAC signatures provide equivalent authentication without requiring certificate management.
Related Articles
Next Steps
Secure your CaptchaAI callback endpoints — get your API key and implement signature validation.
Related guides:
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.