Skip to main content

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

  1. Configure a webhook URL: When making an API request, include a callback_url parameter
  2. Receive notifications: Wordsmith will send a POST request to your URL when processing completes
  3. Verify signatures: Use the webhook secret to verify that requests are genuinely from Wordsmith
  4. 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"
  }'

Webhook payload format

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

id
string
The unique session ID for the completed question
status
string
The final status: "completed" or "error"
answer
string
The assistant’s response. Present when status is "completed", contains error message when status is "error"
attachments
array
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

  1. Extract the timestamp and signature from the Wordsmith-Signature header
  2. Create the signed payload by concatenating the timestamp and the request body: {timestamp}.{payload}
  3. Generate the expected signature using HMAC-SHA256 with your webhook secret
  4. Compare the expected signature with the received signature using a constant-time comparison
  5. 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