NeuroCaptcha

Webhooks

Get the result without polling: a signed POST to your callbackUrl when the task is finished, with three delivery attempts.

Bagian ini ditampilkan dalam bahasa Inggris.

Di halaman ini

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.

createTask
{
  "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#

Webhook request
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:

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.

Header
X-Signature: t=<unix time>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>
  1. Split the header by , and take t and v1.
  2. Compute HMAC-SHA256 with the secret as the key over t, a dot and the raw body — the bytes as received, before JSON parsing.
  3. Compare the hex result with v1 in constant time.
  4. Reject requests whose t differs 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 "", 204

Testing#

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.