About webhooks
Webhooks allow you to receive real-time notifications when asynchronous operations complete in Wordsmith.
Instead of polling the API to check if a task is finished, you can configure a webhook URL to receive callbacks when processing completes.
How webhooks work
- Configure a webhook URL: When making an API request, include a
callback_url
parameter
- Receive notifications: Wordsmith will send a POST request to your URL when processing completes
- Verify signatures: Use the webhook secret to verify that requests are genuinely from Wordsmith
- Process the data: Handle the notification data in your application
Setting up webhooks
Step 1: Create an API key with webhook secret
When creating an API key, you can optionally provide a webhook secret. If you don’t provide one, Wordsmith will automatically generate a secure secret for you.
Step 2: Use webhooks in your requests
Include a callback_url
when making async requests (sync_mode
is false
):
curl -X POST "https://api.wordsmith.ai/api/v1/assistants/default/questions" \
-H "Authorization: Bearer sk-ws-api1-your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"question": "Please analyze this contract",
"attachments": [
{
"upload_job_id": "upload_abc123def456"
}
],
"sync_mode": false,
"callback_url": "https://your-app.com/webhooks/wordsmith"
}'
When processing completes, Wordsmith sends a POST request to your callback URL with the following payload:
{
"id": "session_abc123",
"status": "completed",
"answer": "Based on my analysis of the contract...",
"attachments": [
{
"file_name": "contract_analysis.pdf",
"content_type": "application/pdf",
"content_length": 245760,
"url": "https://files.wordsmith.ai/signed-url-here"
}
]
}
Payload fields
The unique session ID for the completed question
The final status: "completed"
or "error"
The assistant’s response. Present when status
is "completed"
, contains
error message when status
is "error"
Array of generated files (e.g., analysis reports, summaries). Only present
when status
is "completed"
Validating webhook signatures
To ensure webhook requests are genuinely from Wordsmith and haven’t been tampered with, you should validate the signature before processing the payload.
About signature validation
Wordsmith uses HMAC-SHA256 to sign webhook payloads with your webhook secret. The signature is included in the Wordsmith-Signature
header and follows this format:
t=1234567890,v1=abc123def456...
Where:
t
is the timestamp when the signature was created
v1
is the HMAC-SHA256 signature
Signature verification process
- Extract the timestamp and signature from the
Wordsmith-Signature
header
- Create the signed payload by concatenating the timestamp and the request body:
{timestamp}.{payload}
- Generate the expected signature using HMAC-SHA256 with your webhook secret
- Compare the expected signature with the received signature using a constant-time comparison
- Verify the timestamp is within 60 seconds of the current time
Example implementations
Python
import hashlib
import hmac
import time
from typing import Optional
def verify_webhook_signature(payload: str, signature_header: str, secret: str) -> bool:
"""
Verify that the webhook payload was sent from Wordsmith.
Args:
payload: The raw request body
signature_header: The Wordsmith-Signature header value
secret: Your webhook secret
Returns:
True if signature is valid, False otherwise
"""
if not signature_header:
return False
# Parse the signature header
try:
# Extract timestamp and signature
parts = signature_header.split(',')
timestamp_part = parts[0]
signature_part = parts[1]
timestamp = int(timestamp_part.split('=')[1])
signature = signature_part.split('=')[1]
except (IndexError, ValueError):
return False
# Check timestamp tolerance (60 seconds)
current_time = int(time.time())
if abs(current_time - timestamp) > 60:
return False
# Create the signed payload
signed_payload = f"{timestamp}.{payload}"
# Generate expected signature
expected_signature = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Compare signatures using constant-time comparison
return hmac.compare_digest(signature, expected_signature)
# Usage in Flask/FastAPI
@app.route('/webhooks/wordsmith', methods=['POST'])
def webhook_handler():
payload = request.get_data(as_text=True)
signature = request.headers.get('Wordsmith-Signature')
if not verify_webhook_signature(payload, signature, 'your_webhook_secret'):
return 'Invalid signature', 401
# Process the webhook payload
data = request.get_json()
# ... handle the webhook data
return 'OK', 200
JavaScript/Node.js
const crypto = require("crypto");
function verifyWebhookSignature(payload, signatureHeader, secret) {
if (!signatureHeader) {
return false;
}
try {
// Parse the signature header
const parts = signatureHeader.split(",");
const timestampPart = parts[0];
const signaturePart = parts[1];
const timestamp = parseInt(timestampPart.split("=")[1]);
const signature = signaturePart.split("=")[1];
// Check timestamp tolerance (60 seconds)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - timestamp) > 60) {
return false;
}
// Create the signed payload
const signedPayload = `${timestamp}.${payload}`;
// Generate expected signature
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(signedPayload, "utf8")
.digest("hex");
// Compare signatures using constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expectedSignature, "hex")
);
} catch (error) {
return false;
}
}
// Usage in Express
app.post(
"/webhooks/wordsmith",
express.raw({ type: "application/json" }),
(req, res) => {
const payload = req.body.toString();
const signature = req.headers["wordsmith-signature"];
if (!verifyWebhookSignature(payload, signature, "your_webhook_secret")) {
return res.status(401).send("Invalid signature");
}
// Process the webhook payload
const data = JSON.parse(payload);
// ... handle the webhook data
res.send("OK");
}
);
Ruby
require 'openssl'
require 'time'
def verify_webhook_signature(payload, signature_header, secret)
return false unless signature_header
begin
# Parse the signature header
parts = signature_header.split(',')
timestamp_part = parts[0]
signature_part = parts[1]
timestamp = timestamp_part.split('=')[1].to_i
signature = signature_part.split('=')[1]
# Check timestamp tolerance (60 seconds)
current_time = Time.now.to_i
return false if (current_time - timestamp).abs > 60
# Create the signed payload
signed_payload = "#{timestamp}.#{payload}"
# Generate expected signature
expected_signature = OpenSSL::HMAC.hexdigest(
OpenSSL::Digest.new('sha256'),
secret,
signed_payload
)
# Compare signatures using constant-time comparison
Rack::Utils.secure_compare(signature, expected_signature)
rescue
false
end
end
# Usage in Rails/Sinatra
post '/webhooks/wordsmith' do
payload = request.body.read
signature = request.env['HTTP_WORDSMITH_SIGNATURE']
unless verify_webhook_signature(payload, signature, 'your_webhook_secret')
halt 401, 'Invalid signature'
end
# Process the webhook payload
data = JSON.parse(payload)
# ... handle the webhook data
'OK'
end
Testing webhook signatures
You can test your signature verification implementation using these values:
- Secret:
whsec_test_secret_123
- Payload:
{"id":"test","status":"completed"}
- Timestamp:
1234567890
- Expected signature:
c60c0cc7241d79e8bf2a88fdc6ce257c2fd547048bb244495309b27ad07884bf
- Header value:
t=1234567890,v1=c60c0cc7241d79e8bf2a88fdc6ce257c2fd547048bb244495309b27ad07884bf
Security best practices
Store secrets securely
- Never hardcode webhook secrets in your application
- Use environment variables or secure configuration management
- Rotate webhook secrets periodically
Validate all webhooks
- Always verify webhook signatures before processing
- Use constant-time comparison functions to prevent timing attacks
- Check timestamp tolerance to prevent replay attacks
Handle errors gracefully
- Return appropriate HTTP status codes (401 for invalid signatures)
- Log failed signature verifications for monitoring
- Don’t expose sensitive information in error messages
Use HTTPS
- Always use HTTPS for webhook endpoints
- Validate that callback URLs use secure protocols
- Consider using certificate pinning for additional security
Troubleshooting
Common issues
Signature verification fails
- Ensure you’re using the correct webhook secret
- Check that the payload hasn’t been modified by middleware
- Verify you’re parsing the signature header correctly
Webhook not received
- Check that your endpoint is publicly accessible
- Verify the callback URL is correct and uses HTTPS
- Ensure your server can handle POST requests
Timestamp validation fails
- Check that your server’s clock is synchronized
- Verify you’re using the correct tolerance (60 seconds)
- Ensure you’re parsing the timestamp correctly
Testing locally
For local development, you can use tools like ngrok to expose your local server:
# Install ngrok
npm install -g ngrok
# Expose your local server
ngrok http 3000
# Use the ngrok URL as your callback_url
# https://abc123.ngrok.io/webhooks/wordsmith
Rate limiting
Webhook delivery is subject to rate limits to prevent abuse. If you receive too many webhooks in a short period, some may be dropped. Implement idempotency in your webhook handlers to handle potential duplicate deliveries.
Next steps