Webhooks
Get the result without polling: a signed POST to your callbackUrl when the task is finished, with three delivery attempts.
Cette section est affichée en anglais.
Pass callbackUrl to createTask (or pingback to in.php). When the task is finished — solved or failed — we send a POST with JSON to that address. getTaskResult keeps working as well.
{
"clientKey": "YOUR_API_KEY",
"task": {
"type": "RecaptchaV2TaskProxyless",
"websiteURL": "https://www.google.com/recaptcha/api2/demo",
"websiteKey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
},
"callbackUrl": "https://example.com/captcha-webhook"
}What we send#
POST /captcha-webhook HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: captcha-api-webhooks/1
X-Task-Id: 7781
X-Signature: t=1793370008,v1=6c1f0e2b9d4a…
{"taskId":7781,"errorId":0,"status":"ready","solution":{"gRecaptchaResponse":"03AFcWeA6Z3vq0VhGm1oS8r0bAxKk4…","userAgent":""},"cost":"0.000600","createTime":1793370000,"endTime":1793370007,"solveCount":1}The body is the getTaskResult answer plus taskId (without ip). A failed task:
{"taskId":7782,"errorId":12,"errorCode":"ERROR_CAPTCHA_UNSOLVABLE","errorDescription":"the task could not be solved"}Delivery and retries#
- Answer with any 2xx status within 5 seconds; the response body is ignored.
- Up to 3 attempts: at once, 2 seconds later and 6 seconds after that. reCAPTCHA tokens live about 2 minutes, so later retries would be useless — keep polling as a fallback if you need a guarantee.
- Redirects are not followed. The URL must be http(s) and resolve to a public address.
- A delivery can repeat (for example, if your answer was lost) — deduplicate by
taskId. - Webhooks of test keys are delivered and signed in the same way.
Signature#
Every request is signed with the webhook secret of your account (whsec_…). Find it in Dashboard → Notifications; you can rotate it there — the new secret applies at once.
X-Signature: t=<unix time>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>- Split the header by
,and taketandv1. - Compute HMAC-SHA256 with the secret as the key over
t, a dot and the raw body — the bytes as received, before JSON parsing. - Compare the hex result with
v1in constant time. - Reject requests whose
tdiffers from your clock by more than 5 minutes — this stops replays.
Verification examples#
import hashlib
import hmac
import json
import os
import time
from flask import Flask, abort, request
SECRET = os.environ["WEBHOOK_SECRET"] # whsec_… from the dashboard
TOLERANCE = 300 # seconds
def verify(header: str, body: bytes) -> bool:
try:
parts = dict(item.split("=", 1) for item in header.split(","))
t, signature = int(parts["t"]), parts["v1"]
except (KeyError, ValueError):
return False
if abs(time.time() - t) > TOLERANCE:
return False
expected = hmac.new(SECRET.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
app = Flask(__name__)
@app.post("/captcha-webhook")
def captcha_webhook():
body = request.get_data() # raw bytes, exactly as sent
if not verify(request.headers.get("X-Signature", ""), body):
abort(401)
event = json.loads(body)
if event["errorId"] == 0:
print(event["taskId"], event["solution"]["gRecaptchaResponse"])
else:
print(event["taskId"], "failed:", event["errorCode"])
return "", 204Testing#
Create a task with a test key and callbackUrl pointing at your endpoint: the webhook arrives within seconds and is signed like a live one. A local server needs a public URL — use a tunnel; private and local addresses are rejected.