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

# Webhooks

> Receive real-time notifications when documents are processed

## Overview

Webhooks allow your application to receive real-time HTTP notifications when events occur in FieldWise. Instead of polling the API, webhooks push data to your endpoint instantly.

## Supported Events

| Event                           | Description                     |
| ------------------------------- | ------------------------------- |
| `document.processing.started`   | Document processing has begun   |
| `document.processing.completed` | Document processed successfully |
| `document.processing.failed`    | Document processing failed      |

## Webhook Payload

```json theme={null}
{
  "event": "document.processing.completed",
  "timestamp": "2026-01-16T10:09:57.792Z",
  "data": {
    "task_identifier": "DocumentExtraction:a29f03e7-5a95-49a6-a845-ef90b15d0b31",
    "document_id": 164,
    "external_id": null,
    "status": "completed",
    "message": "Document processing completed",
    "file_name": "invoice.pdf"
  }
}
```

### Payload Fields

| Field                  | Type            | Description                            |
| ---------------------- | --------------- | -------------------------------------- |
| `event`                | string          | Event type (dot-notation)              |
| `timestamp`            | string          | ISO 8601 timestamp                     |
| `data.task_identifier` | string          | Unique task ID for tracking            |
| `data.document_id`     | integer \| null | Document ID (available on completion)  |
| `data.external_id`     | string \| null  | Your custom reference ID               |
| `data.status`          | string          | `processing`, `completed`, or `failed` |
| `data.message`         | string          | Human-readable status description      |
| `data.file_name`       | string          | Original file name                     |

## Security Headers

Every webhook includes these headers:

| Header                  | Description                 |
| ----------------------- | --------------------------- |
| `X-FieldWise-Signature` | HMAC-SHA256 signature       |
| `X-FieldWise-Timestamp` | Timestamp used in signature |
| `X-FieldWise-Event`     | Event type                  |

## Signature Verification

**Always verify webhook signatures** to ensure requests come from FieldWise.

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifyWebhook(rawBody, signature, timestamp, secret) {
    // 1. Check timestamp is recent (5 min)
    const age = Math.abs(Date.now() - new Date(timestamp).getTime()) / 1000;
    if (age > 300) return false;
    
    // 2. Compute expected signature
    const message = `${timestamp}.${rawBody}`;
    const expected = crypto
      .createHmac('sha256', secret)
      .update(message)
      .digest('hex');
    
    // 3. Compare (constant-time)
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    );
  }

  // Express.js example
  app.post('/webhooks', express.raw({type: 'application/json'}), (req, res) => {
    const signature = req.headers['x-fieldwise-signature'];
    const timestamp = req.headers['x-fieldwise-timestamp'];
    
    if (!verifyWebhook(req.body.toString(), signature, timestamp, WEBHOOK_SECRET)) {
      return res.status(401).send('Invalid signature');
    }
    
    const event = JSON.parse(req.body);
    console.log('Received:', event.event, event.data.status);
    
    res.status(200).send('OK');
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  from datetime import datetime, timezone
  from flask import Flask, request

  def verify_webhook(payload, signature, timestamp, secret):
      # Check timestamp is recent
      webhook_time = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
      age = abs((datetime.now(timezone.utc) - webhook_time).total_seconds())
      if age > 300:
          return False
      
      # Compute expected signature
      message = f"{timestamp}.{payload}"
      expected = hmac.new(
          key=secret.encode(),
          msg=message.encode(),
          digestmod=hashlib.sha256
      ).hexdigest()
      
      return hmac.compare_digest(signature, expected)

  # Flask example
  @app.route('/webhooks', methods=['POST'])
  def handle_webhook():
      signature = request.headers.get('X-FieldWise-Signature')
      timestamp = request.headers.get('X-FieldWise-Timestamp')
      
      if not verify_webhook(request.data.decode(), signature, timestamp, WEBHOOK_SECRET):
          return 'Invalid signature', 401
      
      event = request.json
      print(f"Received: {event['event']} - {event['data']['status']}")
      
      return 'OK', 200
  ```
</CodeGroup>

## Retry Policy

Failed deliveries are automatically retried:

| Attempt   | Delay       |
| --------- | ----------- |
| 1st retry | 30 seconds  |
| 2nd retry | 60 seconds  |
| 3rd retry | 120 seconds |

After 3 failed attempts, the webhook is marked as failed.

## Best Practices

<CardGroup cols={2}>
  <Card title="Respond Quickly" icon="bolt">
    Return `200 OK` immediately, then process asynchronously. Timeout is 30 seconds.
  </Card>

  <Card title="Handle Duplicates" icon="clone">
    Use `task_identifier` to deduplicate. Webhooks may be delivered more than once.
  </Card>

  <Card title="Use HTTPS" icon="lock">
    Always use HTTPS endpoints in production to protect webhook payloads.
  </Card>

  <Card title="Verify Signatures" icon="shield-check">
    Always verify the `X-FieldWise-Signature` header before processing.
  </Card>
</CardGroup>

## Managing Webhooks

Configure webhooks directly in the FieldWise dashboard under **Settings → Webhooks**. From there you can:

* Create new webhooks
* Edit webhook settings
* Delete webhooks
* Send test payloads
* Rotate secrets
