Endpoint callback CaptchaAI adalah URL publik: siapa pun yang mengetahuinya bisa mengirim request ke sana. Aturan kerjanya cukup satu kalimat — tolak setiap request yang asal-usulnya tidak bisa Anda buktikan. Tutorial ini memasang empat lapis pemeriksaan pada endpoint pingback Anda: task ID yang memang Anda daftarkan, tanda tangan HMAC pada URL, IP allowlist, dan proteksi replay — dengan contoh Python (Flask) dan Node.js (Express).
Bagaimana pingback CaptchaAI sampai ke server Anda
Alurnya tiga langkah:
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
Perhatikan langkah 3: itu request GET biasa tanpa autentikasi. Bukti asal-usul harus Anda bangun sendiri, dan satu-satunya rahasia yang bisa dititipkan ada pada URL callback yang Anda kirim di langkah 1.
Kasus yang sering muncul di tim automation lokal: agensi price monitoring menjalankan worker Flask di VPS Singapura (ap-southeast-1) dengan /captcha/callback terbuka ke internet. Begitu URL itu bocor lewat log, tangkapan layar di grup Telegram, atau repo yang tidak sengaja public, siapa pun bisa menyuntikkan token palsu ke antrean hasil Anda.
Empat lapis validasi dan apa yang dicegahnya
Lapis-lapis berikut saling melengkapi, bukan pilihan alternatif:
| Lapis | Mencegah | Implementasi |
|---|---|---|
| Verifikasi task ID | Task acak atau tidak dikenal disuntikkan | Simpan ID pending, tolak ID di luar daftar |
| Tanda tangan HMAC | URL ditebak, callback palsu | Tandatangani URL callback dengan secret |
| IP allowlist | Request dari server yang tidak sah | Izinkan hanya IP sumber CaptchaAI |
| Proteksi replay | Callback sah yang dikirim ulang | Cek timestamp + aturan sekali pakai |
| HTTPS | Penyadapan dan man-in-the-middle | TLS pada endpoint callback |
Minimum yang layak dibawa ke production: lapis 1 + lapis 2 + HTTPS; lapis 3 dan 4 menyusul sesuai risiko workflow Anda.
Lapis 1: terima callback hanya untuk task ID yang Anda daftarkan
Pemeriksaan paling murah: simpan setiap task ID yang dikembalikan in.php, lalu tolak callback yang membawa ID di luar daftar itu.
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
Node.js (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);
Set di memori hanya cukup untuk satu proses. Begitu ada lebih dari satu worker atau deploy ulang, pindahkan daftar pending ke Redis atau database — jika tidak, callback yang sah ditolak hanya karena mendarat di proses lain.
Lapis 2: tanda tangan HMAC pada URL callback
Task ID bisa ditebak kalau formatnya pendek dan berurutan. Tambahkan parameter token berisi HMAC-SHA256 dari task ID, memakai secret yang hanya ada di server Anda.
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
Node.js
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);
});
Gunakan URL hasil generate saat submit: pingback=https://your-server.com/captcha/callback?token=HMAC_SIGNATURE.
Tiga detail menentukan apakah lapis ini berguna: secret minimal 32 karakter acak yang dibaca dari environment variable, perbandingan constant-time (hmac.compare_digest, crypto.timingSafeEqual) alih-alih ==, dan nilai token yang tidak pernah tercatat di log. Khusus di Express, panjang kedua buffer harus sama sebelum dibandingkan — jika berbeda, timingSafeEqual melempar error alih-alih mengembalikan false.
Lapis 3: batasi sumber callback dengan IP allowlist
Kalau endpoint itu memang hanya melayani CaptchaAI, tutup sisanya di lapisan aplikasi — atau lebih baik lagi, di firewall atau security group.
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
Node.js (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();
});
Catatan: Hubungi dukungan CaptchaAI untuk daftar IP sumber callback terkini. Jika endpoint berada di belakang reverse proxy atau load balancer,
request.remote_addrakan berisi IP proxy, bukan IP pengirim asli — konfigurasikanX-Forwarded-Fordan percayai header itu hanya jika datang dari proxy Anda sendiri.
Lapis 4: hentikan replay dengan batas waktu dan aturan sekali pakai
Callback yang sah pun bisa direkam lalu dikirim ulang. Dua pemeriksaan menutup celah ini: tolak callback yang timestamp-nya lewat batas waktu, dan pastikan satu task ID hanya diproses sekali.
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
Untuk workflow pembayaran atau pendaftaran, simpan penanda "sudah diproses" di database dengan unique constraint: set memori hilang saat restart, sedangkan constraint sekaligus menyelesaikan race condition antar-worker.
Sisi operasional: deploy, latensi, dan log
- Wilayah deploy.
ap-southeast-1(Singapura),ap-southeast-3(Jakarta), maupunasia-southeast2(Jakarta) sama-sama masuk akal; yang menentukan bukan jarak, melainkan seberapa cepat endpoint membalas 200. - Balas dulu, proses belakangan. Terima payload, masukkan ke antrean, balas 200, lalu proses di background.
- Polling tetap dipertahankan.
res.phptetap tersedia; jadikan polling sebagai jaring pengaman untuk task yang callback-nya tidak pernah tiba. Penagihan CaptchaAI berbasis thread — BASIC $15/bulan dengan 5 thread, STANDARD $30/bulan dengan 15 thread, penyelesaian tanpa batas per thread — sehingga fallback polling tidak menambah biaya per penyelesaian. - Log seperlunya. Catat status validasi beserta alasannya, bukan isi token. Prinsip menyimpan data seminimal mungkin juga selaras dengan UU Pelindungan Data Pribadi (UU 27/2022).
Masalah callback yang umum dan cara memperbaikinya
| Gejala | Penyebab | Perbaikan |
|---|---|---|
| Semua callback ditolak 403 | IP allowlist tidak memuat IP CaptchaAI, atau yang terbaca IP proxy | Verifikasi IP terkini ke dukungan; baca X-Forwarded-For dari proxy tepercaya |
| Verifikasi HMAC selalu gagal | Task ID saat submit dan saat callback tidak sama persis | Tandatangani ID persis seperti yang dikembalikan in.php |
timingSafeEqual melempar error |
Panjang buffer token dan expected berbeda | Periksa panjang lebih dulu, baru bandingkan |
| Callback sah ditolak setelah deploy | Daftar task pending hanya ada di memori satu proses | Pindahkan ke Redis atau database |
| Satu hasil terproses dua kali | Race condition pada callback bersamaan | Gunakan operasi set atomik atau unique constraint database |
Checklist sebelum deploy
- Endpoint hanya melayani HTTPS dengan sertifikat valid.
- Secret HMAC dibaca dari environment variable, bukan dari repo.
- Daftar task pending ada di storage bersama, bukan memori proses.
- Pemeriksaan timestamp dan aturan sekali pakai aktif untuk workflow sensitif.
- Fallback polling ke
res.phpberjalan untuk task tanpa callback. - Ada alert saat rasio callback ditolak naik mendadak.
Pertanyaan yang sering muncul
Apakah URL callback wajib memakai HTTPS?
Perlakukan sebagai wajib. Di HTTP biasa, task ID, token solusi, dan tanda tangan HMAC melintas dalam bentuk terbaca, sehingga penyadap di jaringan bisa merekam lalu mengirim ulang request yang sama.
Berapa cepat endpoint callback harus merespons?
Idealnya di bawah satu detik: validasi, masukkan ke antrean, balas 200, lalu kerjakan sisanya di worker terpisah. Menjalankan parsing HTML atau alur login di dalam handler callback adalah penyebab timeout yang paling sering ditemukan.
Bagaimana cara menguji validasi callback sebelum masuk production?
Jalankan endpoint di lokal, ekspos sementara lewat tunnel HTTPS, lalu kirim satu task uji dengan pingback menunjuk ke URL tunnel itu. Uji juga jalur negatifnya: task ID acak, token HMAC salah, dan callback ganda masing-masing harus menghasilkan 403, 403, dan 409.
Perlukah secret HMAC berbeda untuk setiap task?
Tidak perlu. Satu secret per layanan sudah cukup karena tanda tangannya unik per task ID. Yang penting: rotasi berkala, dan terima dua secret sekaligus selama masa transisi agar callback task yang masih berjalan tidak ikut ditolak.
Apa yang harus dilakukan kalau callback untuk satu task datang dua kali?
Buat handler Anda idempoten: pemrosesan kedua berhenti di pemeriksaan sekali pakai dan mengembalikan 409, bukan menimpa hasil pertama. Pengiriman ganda itu normal pada sistem webhook mana pun.
Baca berikutnya
- pola penanganan error pada callback — saat callback gagal atau tidak pernah tiba.
- panduan URL callback dan webhook CaptchaAI — mengatur parameter
pingbackdari awal. - pola notifikasi task lewat pingback — mendistribusikan hasil ke beberapa consumer.