Webhooks
Get the result without polling: a signed POST to your callbackUrl when the task is finished, with three delivery attempts.
本節內容以英文顯示。
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.