> ## Documentation Index
> Fetch the complete documentation index at: https://wordsmithai.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Learn how to receive real-time notifications from Wordsmith using webhooks with signature verification for security.

## 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`):

```bash theme={null}
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:

```json theme={null}
{
  "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

<ResponseField name="id" type="string">
  The unique session ID for the completed question
</ResponseField>

<ResponseField name="status" type="string">
  The final status: `"completed"` or `"error"`
</ResponseField>

<ResponseField name="answer" type="string" nullable>
  The assistant's response. Present when `status` is `"completed"`, contains
  error message when `status` is `"error"`
</ResponseField>

<ResponseField name="attachments" type="array" nullable>
  Array of generated files (e.g., analysis reports, summaries). Only present
  when `status` is `"completed"`
</ResponseField>

## 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

```python theme={null}
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

```javascript theme={null}
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

```ruby theme={null}
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:

```bash theme={null}
# 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

* [API Reference](/api-reference) - Explore all available endpoints
* [Authentication](/authentication) - Learn about API key authentication
* [Quickstart](/quickstart) - Get started with the Wordsmith API
