Webhooks

Receive review results automatically via secure webhook notifications.

How Webhooks Work

When a review is completed, we'll POST the results to the webhook_url you provided when submitting the content.

You submit content with a webhook_url
Reviewer completes the review
We POST results to your webhook_url
Your app processes the results

Webhook Payload

The payload structure depends on the reviewer's decision:

Accepted (No Changes)

When the reviewer accepts the AI response without modifications:

{
"review_id": 42,
"external_id": "TICKET-1234",
"decision": "accepted",
"edited_output": null,
"rejection_reason": null,
"metadata": {
"ticket_id": "1234",
"customer_id": "C789"
}
}

Use your original AI output - the reviewer approved it as-is!

Edited (Improved)

When the reviewer improves the AI response:

{
"review_id": 43,
"external_id": "TICKET-1235",
"decision": "edited",
"edited_output": "To return your order, please visit our returns portal...",
"rejection_reason": null,
"metadata": {
"ticket_id": "1235",
"customer_id": "C790"
}
}

Use the edited_output instead of your original - it's been improved by the reviewer!

Rejected

When the reviewer rejects the AI response:

{
"review_id": 44,
"external_id": "TICKET-1236",
"decision": "rejected",
"edited_output": null,
"rejection_reason": "Response contains inaccurate information",
"metadata": {
"ticket_id": "1236",
"customer_id": "C791"
}
}

Don't use the original output. Check rejection_reason and regenerate content or escalate to human support.

Why isn't the original AI output included?

The webhook focuses on review results only. Use the external_id to look up the original content in your system. This keeps payloads small and forces proper data architecture.

Webhook Delivery

Method POST
Content-Type application/json
Timeout 10 seconds
Retries Up to 3 attempts with exponential backoff (20s, 40s, 60s)
Success Any 2xx status code
Failure After 3 failed attempts, status changes to webhook_failed

Respond quickly!

Your webhook endpoint must respond with a 2xx status code within 10 seconds. Process heavy workloads asynchronously.

Webhook Security

All webhooks include HMAC-SHA256 signatures to verify authenticity and prevent tampering.

You MUST verify webhook signatures!

Without verification, attackers could send fake review results to your endpoint. Always validate the signature before processing.

Security Headers

Every webhook request includes these headers:

Header Format Description
X-Signature-256 sha256=<hex> HMAC-SHA256 signature of request body
X-Webhook-Timestamp <unix_timestamp> When webhook was sent (seconds since epoch)
X-Webhook-ID <uuid> Unique identifier for this delivery
X-Tenant-ID <tenant_id> Your tenant/organization identifier

Signature Verification

Step 1: Get Your Webhook Secret

Sign in and navigate to Settings to generate a webhook secret.

Webhook secrets start with cya_whs_

Step 2: Verify Each Webhook

  1. Extract the raw request body (before parsing JSON)
  2. Extract signature from X-Signature-256 header (remove "sha256=" prefix)
  3. Compute HMAC-SHA256 hash of raw body using your webhook secret
  4. Compare computed signature with received signature (constant-time comparison)
  5. Verify timestamp is recent (within 5 minutes) to prevent replay attacks

Verification Code Example

Here's a language-agnostic example showing the verification flow:

webhook-verification-flow.js
// 1. Extract signature and timestamp from webhook headers
signature = request.headers['X-CheckYourAI-Signature']
timestamp = request.headers['X-CheckYourAI-Timestamp']
// 2. Verify timestamp is recent (within 5 minutes)
current_time = current_unix_timestamp()
if (abs(current_time - timestamp) > 300) {
return error("Webhook timestamp too old")
}
// 3. Construct the signed payload
signed_payload = timestamp + "." + request.body
// 4. Compute HMAC-SHA256 signature
expected_signature = hmac_sha256(
key: YOUR_WEBHOOK_SECRET,
message: signed_payload
)
// 5. Compare signatures (use constant-time comparison!)
if (!constant_time_compare(signature, expected_signature)) {
return error("Invalid webhook signature")
}
// 6. Signature is valid - process webhook safely
process_webhook(request.body)

Implementation Notes:

  • Timestamp validation: Prevents replay attacks by rejecting old webhooks
  • Constant-time comparison: Prevents timing attacks when comparing signatures
  • HMAC-SHA256: Use your language's crypto library (crypto in Node.js, hashlib in Python, etc.)

Complete Webhook Handler Example

Here's a complete example handling all decision types:

webhook-handler.js
app.post('/webhooks/review-completed', async (req, res) => {
const params = req.body;
res.status(200).json({received: true});
try {
const original = await db.getSubmissionByExternalId(params.external_id);
if (params.decision === 'accepted') {
await processApprovedContent(original.ai_output, params.metadata);
} else if (params.decision === 'edited') {
await processApprovedContent(params.edited_output, params.metadata);
} else if (params.decision === 'rejected') {
await handleRejection(original, params.rejection_reason, params.metadata);
}
} catch (error) {
console.error('Error:', error);
}
});

Best Practices

Respond Immediately

Return 200 OK as quickly as possible, then process the payload asynchronously. Don't wait for database writes or external API calls.

Always Verify Signatures

NEVER skip signature verification, even in development. Use constant-time comparison to prevent timing attacks.

Check Timestamps

Reject webhooks with old timestamps (>5 minutes) to prevent replay attacks.

Handle All Decision Types

Your code should handle "accepted", "edited", and "rejected" decisions appropriately.

Make Webhook Processing Idempotent

Use X-Webhook-ID to track processed webhooks and avoid duplicate processing if we retry.

Testing Webhooks Locally

Use a Tunneling Service

Expose your local development server to receive webhooks:

# Using ngrok
ngrok http 4000
# Or using localtunnel
npx localtunnel --port 4000

Then use the provided public URL as your webhook_url when testing.

Next Steps