Webhooks
Skip polling — register a callback URL and Grep will POST to it when the job is done. HMAC-signed, retried up to three times.
Register a webhook
Pass webhook_url when creating a job. Use HTTPS for production webhooks; HTTP is only appropriate for local development. Production deployments also reject private network targets.
curl -X POST "https://api.grep.ai/api/v2/research" \
-H "Authorization: Bearer $GREP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"question": "Due diligence on Stripe",
"effort": "medium",
"webhook_url": "https://your-app.com/grep/webhooks/completion"
}'webhook_url— POSTed once when the job reaches a terminal state (completed or failed).
HMAC signatures
Every webhook delivery includes an X-Signature-SHA256 header. The signature is base64(HMAC-SHA256(api_key, raw JSON body)). Verify before trusting the payload — this protects against spoofed callbacks.
POST /grep/webhooks/completion HTTP/1.1
Host: your-app.com
Content-Type: application/json
X-Signature-SHA256: QzJrZ1hnbmJZVHZjTzRweExuZmlEZ0twYkpqVkJkR0kzT2pQbm1PTz0=
X-Request-ID: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Grep-Original-Request-ID: 5d4b8e10-1a4c-4f3a-8bd7-3a2e1c4d5b8f
X-Grep-Job-ID: 550e8400-e29b-41d4-a716-446655440000FastAPI verification example. Note the constant-time compare:
import base64
import hmac
import hashlib
import os
from fastapi import FastAPI, Header, HTTPException, Request
API_KEY = os.environ["GREP_API_KEY"]
app = FastAPI()
@app.post("/grep/webhooks/completion")
async def handle_webhook(
request: Request,
x_signature_sha256: str = Header(..., alias="X-Signature-SHA256"),
):
body = await request.body()
# Signature is base64(HMAC-SHA256(api_key, raw_json_body)).
expected = base64.b64encode(
hmac.new(API_KEY.encode(), body, hashlib.sha256).digest()
).decode()
if not hmac.compare_digest(expected, x_signature_sha256):
raise HTTPException(401, "bad signature")
# ... handle the verified payload ...
return {"ok": True}Correlation headers
Three v2 headers help you correlate webhooks with originating requests and downstream logs:
| Header | Purpose |
|---|---|
X-Request-ID | Per-delivery ID. Unique to this webhook attempt — useful when you need to dedupe across retries. |
X-Grep-Original-Request-ID | Request ID of the originating POST /research call. Stable across all webhooks for the same job. |
X-Grep-Job-ID | The job_id this webhook is about. Same as the body field — duplicated as a header for routing convenience. |
Retry policy
If your endpoint returns a non-2xx status (or times out), Grep retries with exponential backoff.
- Up to 3 attempts per webhook.
- Total retry budget is 30 seconds — beyond that the delivery is dropped.
- Any 2xx response is success. We don't inspect the body.
v1 vs v2 webhooks
If you're migrating from v1, here's what changed:
| Aspect | v1 | v2 |
|---|---|---|
| Signature header | X-Parcha-Signature | X-Signature-SHA256 |
| Correlation headers | Job ID only, in body | X-Request-ID, X-Grep-Original-Request-ID, X-Grep-Job-ID |
| Body shape | Flat — fields at top level | Flat — event, job_id, status, report, and optional structured_output |