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

# Webhooks

# Webhooks

Webhooks allow you to receive real-time updates about results (and soon statuses) from the Parcha API. This guide explains how to use webhooks, including how to set them up, secure them with HMAC signatures, and test them.

## Setting Up Webhooks

Parcha API supports two types of webhooks:

1. **Job Webhooks** (`webhook_url`): Receive notifications when the entire job is completed
2. **Tool Webhooks** (`tool_webhook_url`): Receive real-time updates each time an LLM tool is executed during the job

You can specify either or both webhook URLs when initiating a job:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://api.parcha.ai/api/v1/startKYBAgentJob' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "agent_key": "your-agent-key",
    "kyb_schema": {
      "id": "case-001",
      "self_attested_data": {
        "business_name": "Example Corp",
        "incorporation_documents": [
          {
            "url": "https://example.com/document.pdf",
            "type": "Document",
            "file_name": "incorporation.pdf"
          }
        ]
      }
    },
    "webhook_url": "https://your-webhook-url.com/job-callback",
    "tool_webhook_url": "https://your-webhook-url.com/tool-callback"
  }'
  ```

  ```python Python theme={null}
  import requests

  api_key = 'YOUR_API_KEY'
  url = 'https://api.parcha.ai/api/v1/startKYBAgentJob'

  data = {
      "agent_key": "your-agent-key",
      "kyb_schema": {
          "id": "case-001",
          "self_attested_data": {
              "business_name": "Example Corp",
              "incorporation_documents": [
                  {
                      "url": "https://example.com/document.pdf",
                      "type": "Document",
                      "file_name": "incorporation.pdf"
                  }
              ]
          }
      },
      "webhook_url": "https://your-webhook-url.com/job-callback",
      "tool_webhook_url": "https://your-webhook-url.com/tool-callback"
  }

  headers = {
      'Authorization': f'Bearer {api_key}',
      'Content-Type': 'application/json'
  }

  response = requests.post(url, json=data, headers=headers)
  print(response.json())
  ```

  ```typescript TypeScript theme={null}
  import axios from 'axios';

  const apiKey = 'YOUR_API_KEY';
  const url = 'https://api.parcha.ai/api/v1/startKYBAgentJob';

  const data = {
    agent_key: "your-agent-key",
    kyb_schema: {
      id: "case-001",
      self_attested_data: {
        business_name: "Example Corp",
        incorporation_documents: [
          {
            url: "https://example.com/document.pdf",
            type: "Document",
            file_name: "incorporation.pdf"
          }
        ]
      }
    },
    webhook_url: "https://your-webhook-url.com/job-callback",
    tool_webhook_url: "https://your-webhook-url.com/tool-callback"
  };

  axios.post(url, data, {
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json'
    }
  })
  .then(response => console.log(response.data))
  .catch(error => console.error('Error:', error));
  ```
</CodeGroup>

## Webhook Payloads

### Job Webhook Payload

When a job is completed, Parcha API sends a POST request to your webhook URL with a payload containing job status information. **Note: The webhook payload contains job status updates only, not the full check results.**

```json theme={null}
{
  "id": "e85e0cc4-b1e8-40b0-9a3d-83c328eee0f4",
  "agent_id": "remediation-v1",
  "status": "complete",
  "recommendation": "Approve",
  "created_at": "2024-09-18T18:30:00.000000",
  "updated_at": "2024-09-18T18:31:05.094700",
  "input_payload": {
    "id": "case-001",
    "self_attested_data": {
      "business_name": "Example Corp"
    }
  }
}
```

### Fetching Full Check Results

To get the complete check results after receiving a webhook notification, use the `getJobById` endpoint with `include_check_results=true`:

```bash theme={null}
curl -X GET 'https://api.parcha.ai/api/v1/getJobById?job_id=e85e0cc4-b1e8-40b0-9a3d-83c328eee0f4&include_check_results=true' \
-H 'Authorization: Bearer YOUR_API_KEY'
```

<Tip>
  Process webhook payloads quickly and asynchronously. Use the `job_id` from the webhook to fetch full results via the API when needed.
</Tip>

### Tool Webhook Payload

Tool webhooks provide real-time updates for individual tool executions. You can filter events by tool ID. For example, `kyb.incorporation_document_extractor` provides events related to incorporation document checks in the following order:

1. Text extraction results (fastest)
2. Visual verification
3. Fraud detection (slowest)

Here are examples of different tool webhook payloads:

#### Text Extraction Results

The text extraction event is typically the fastest and includes important validation and extracted data fields:

```json theme={null}
{
  "job_id": "74f4634849d747a4b6546dfc35bdde29",
  "agent_instance_id": "c50aee83-45d6-48a2-800d-78e50ab5ea3f",
  "tool_id": "kyb.incorporation_document_extraction_tool",
  "results": {
    "explanation": "I analyzed the provided document and determined it is a valid Certificate of Incorporation for Parcha Labs, Inc. The document contains all necessary information including business name, incorporation date, registered agent address, and filing number.",
    "payload": {
      "extraction_data": {
        "is_valid_document": true,
        "analysis": "This document is a valid Certificate of Incorporation for Parcha Labs, Inc. It contains all the necessary elements of a valid incorporation document, including the business name, incorporation date, registered agent address, jurisdiction, and filing number.",
        "summary": "This document is a Certificate of Incorporation for Parcha Labs, Inc., filed with the Delaware Secretary of State on March 29, 2023. It outlines the company's basic information, including its name, registered agent, purpose, authorized shares, and various articles detailing the corporation's governance and legal matters.",
        "date": "2023-03-29",
        "alerts": null,
        "business_name": "Parcha Labs, Inc.",
        "all_business_names": [
          "Parcha Labs, Inc."
        ],
        "incorporation_date": "2023-03-29",
        "business_address": {
          "type": "Address",
          "street_1": "251 Little Falls Drive",
          "street_2": null,
          "city": "Wilmington",
          "state": "Delaware",
          "country_code": "US",
          "postal_code": "19808"
        },
        "all_extracted_addresses": {
          "registered_agent_address": {
            "type": "Address",
            "street_1": "251 Little Falls Drive",
            "street_2": null,
            "city": "Wilmington",
            "state": "Delaware",
            "country_code": "US",
            "postal_code": "19808"
          },
          "incorporator_address": {
            "type": "Address",
            "street_1": "746 4th Avenue",
            "street_2": null,
            "city": "San Francisco",
            "state": "CA",
            "country_code": "US",
            "postal_code": "94118"
          }
        },
        "business_activity": "To engage in any lawful act or activity for which corporations may be organized under the Delaware General Corporation Law",
        "jurisdiction": "Delaware",
        "business_registration_number": [
          "7379553"
        ]
      }
    },
    "passed": true,
    "answer": "The document is a valid Certificate of Incorporation for Parcha Labs, Inc., containing all necessary information for a Delaware corporation.",
    "alerts": null
  }
}
```

Key fields in the extraction results include:

* `is_valid_document`: Boolean indicating if the document is a valid incorporation document
* `business_name`: The primary name of the business
* `all_business_names`: Array of all business names found in the document
* `incorporation_date`: The date of incorporation
* `business_address`: The primary business address
* `all_extracted_addresses`: Object containing all addresses found in the document, categorized by type
* `business_activity`: The stated purpose or activity of the business
* `jurisdiction`: The jurisdiction where the business is incorporated
* `business_registration_number`: Array of registration numbers found in the document

#### Visual Verification Results

```json theme={null}
{
  "job_id": "74f4634849d747a4b6546dfc35bdde29",
  "agent_instance_id": "92e52dec-4ec1-4dc9-8040-25d9b78a525f",
  "tool_id": "kyb.visual_verification_v3",
  "results": {
    "explanation": "Visual verification of the document features",
    "payload": {
      "visual_inspection": {
        "type": "VisualInspectionResults",
        "comparison": "The Delaware Certificate of Incorporation for Parcha Labs, Inc. is consistent with other Delaware business registration documents reviewed.",
        "reasoning": "The document's authenticity is supported by several key features: (1) The presence of the official Delaware state seal, verifying the document's origin and jurisdiction. (2) The signature of Jeffrey W. Bullock, Secretary of State of Delaware, adds further validation. (3) The document's structure, layout, and content align with the standard format for Delaware Certificates of Incorporation. (4) The absence of any visible alterations or inconsistencies further supports its authenticity.",
        "visual_analysis": {
          "document_analysis_and_classification": {
            "analysis": "The document is a Certificate of Incorporation for Parcha Labs, Inc., filed in Delaware on March 29, 2023.",
            "passed": true
          },
          "seal_verification": {
            "analysis": "The first page contains a Delaware state seal. The seal's design, text content, and unique identifiers are consistent with the expected Delaware state seal.",
            "passed": true,
            "present": true
          },
          "signature_verification": {
            "analysis": "The document contains a signature attributed to Jeffrey W. Bullock, Secretary of State of Delaware.",
            "passed": true,
            "present": true
          },
          "document_layout_and_content_verification": {
            "analysis": "The layout is consistent with a Delaware Certificate of Incorporation.",
            "passed": true
          },
          "authenticity_verification": {
            "analysis": "The document appears authentic based on the presence of the official Delaware seal.",
            "passed": true
          },
          "cross_referencing_and_consistency_check": {
            "analysis": "The business name, address, and key dates are consistent throughout the document.",
            "passed": true
          }
        },
        "inspection_passed": true,
        "summary": "Visual inspection and comparison with other Delaware business registration documents indicate that the Certificate of Incorporation is authentic."
      }
    },
    "passed": true
  }
}
```

#### Fraud Detection Results

```json theme={null}
{
  "job_id": "74f4634849d747a4b6546dfc35bdde29",
  "agent_instance_id": "8fab01c9-2a96-40ad-a01a-46e1b7a88db3",
  "tool_id": "document_fraud_check",
  "results": {
    "explanation": "The document verification analysis reveals significant concerns regarding potential fraud or tampering.",
    "payload": {
      "fraud_verification_data": {
        "verification_passed": false,
        "verification_result": "HIGH_RISK",
        "verification_data": [
          {
            "indicator_id": "has_anomalous_hsv_regions",
            "type": "RISK",
            "category": "modifications",
            "title": "Document contains regions with font color inconsistency",
            "description": "This PDF document contains characters with color artefacts distinctively different from the rest of the document."
          },
          {
            "indicator_id": "is_pdf_unmodified_per_metadata",
            "type": "TRUST",
            "category": "authenticity",
            "title": "No modification in document metadata",
            "description": "Timestamps in this document's metadata do not indicate that the file was incrementally updated or modified."
          },
          {
            "indicator_id": "is_joined_pdf",
            "type": "INFO",
            "category": "known_creator",
            "title": "PDF is likely composed of more documents",
            "description": "This document has both selectable text and image pages."
          }
        ],
        "verification_description": "The document failed the fraud verification check due to high-risk indicators."
      }
    },
    "passed": false
  }
}
```

## HMAC Signature Verification

To ensure the security and authenticity of webhook payloads, Parcha API signs each webhook request with HMAC-SHA256 signatures. Two signatures are provided in the webhook request headers:

1. **Standard Signature** (`X-Signature-SHA256`): Signs the entire JSON payload
2. **Compact Signature** (`parcha-signature-compact`): Signs only the case ID for lightweight verification

### Standard Signature Verification

The standard signature is included in the `X-Signature-SHA256` header and covers the entire request body.

To verify the standard signature:

1. Compute the HMAC-SHA256 signature of the raw request body using your API secret key.
2. Compare this computed signature with the one in the `X-Signature-SHA256` header.

Here's an example of how to verify the signature in Python:

```python theme={null}
import hmac
import hashlib
import base64

def verify_signature(payload, signature, api_secret_key):
    computed_signature = base64.b64encode(
        hmac.new(api_secret_key.encode('utf-8'), payload, digestmod=hashlib.sha256).digest()
    ).decode('utf-8')
    return hmac.compare_digest(computed_signature, signature)

# In your webhook handler:
@app.route('/webhook', methods=['POST'])
def handle_webhook():
    payload = request.get_data()
    signature = request.headers.get('X-Signature-SHA256')

    if verify_signature(payload, signature, YOUR_API_SECRET_KEY):
        # Process the webhook
        data = request.json
        # ... handle the webhook data ...
        return 'Webhook received and verified', 200
    else:
        return 'Invalid signature', 401
```

### Parcha Compact Signature

The compact signature provides a lightweight alternative for verifying webhook authenticity without needing to process the entire payload. This signature is included in the `parcha-signature-compact` header and signs only the case ID from `input_payload.id`.

**When to use the compact signature:**

* When you need quick verification before processing large payloads
* When you want to validate the webhook source using only the case ID
* For implementing rate limiting or deduplication based on case ID
* When you need to verify webhooks in environments with limited payload access

**How the compact signature is generated:**

1. Extract the `id` field from `input_payload` in the webhook payload
2. Compute HMAC-SHA256 of the ID string using your API secret key
3. Base64 encode the resulting digest

Here's an example of how to verify the compact signature:

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib
  import base64
  import json

  def verify_compact_signature(payload_data, compact_signature, api_secret_key):
      """
      Verify the Parcha compact signature using only the case ID.

      Args:
          payload_data: The parsed JSON payload (dict)
          compact_signature: The signature from the parcha-signature-compact header
          api_secret_key: Your API secret key

      Returns:
          bool: True if signature is valid, False otherwise
      """
      # Extract the case ID from the payload
      case_id = payload_data.get('input_payload', {}).get('id')

      if not case_id:
          return False

      # Convert case ID to string and compute signature
      id_string = str(case_id)
      computed_signature = base64.b64encode(
          hmac.new(
              api_secret_key.encode('utf-8'),
              id_string.encode('utf-8'),
              digestmod=hashlib.sha256
          ).digest()
      ).decode('utf-8')

      # Use constant-time comparison to prevent timing attacks
      return hmac.compare_digest(computed_signature, compact_signature)

  # In your webhook handler with both signatures:
  @app.route('/webhook', methods=['POST'])
  def handle_webhook():
      # Get both signatures
      full_signature = request.headers.get('X-Signature-SHA256')
      compact_signature = request.headers.get('parcha-signature-compact')

      # Parse the payload
      payload_data = request.json
      raw_payload = request.get_data()

      # Option 1: Quick verification with compact signature
      if compact_signature and verify_compact_signature(payload_data, compact_signature, YOUR_API_SECRET_KEY):
          # Lightweight verification passed - process webhook
          case_id = payload_data['input_payload']['id']
          # ... quick processing based on case_id ...
          return 'Webhook received', 200

      # Option 2: Full verification with standard signature
      elif full_signature and verify_signature(raw_payload, full_signature, YOUR_API_SECRET_KEY):
          # Full verification passed - process complete payload
          # ... full processing ...
          return 'Webhook received and verified', 200

      else:
          return 'Invalid signature', 401
  ```

  ```typescript TypeScript theme={null}
  import crypto from 'crypto';

  function verifyCompactSignature(
    payloadData: any,
    compactSignature: string,
    apiSecretKey: string
  ): boolean {
    /**
     * Verify the Parcha compact signature using only the case ID.
     *
     * @param payloadData - The parsed JSON payload
     * @param compactSignature - The signature from the parcha-signature-compact header
     * @param apiSecretKey - Your API secret key
     * @returns True if signature is valid, false otherwise
     */

    // Extract the case ID from the payload
    const caseId = payloadData?.input_payload?.id;

    if (!caseId) {
      return false;
    }

    // Convert case ID to string and compute signature
    const idString = String(caseId);
    const computedSignature = crypto
      .createHmac('sha256', apiSecretKey)
      .update(idString)
      .digest('base64');

    // Use constant-time comparison to prevent timing attacks
    return crypto.timingSafeEqual(
      Buffer.from(computedSignature),
      Buffer.from(compactSignature)
    );
  }

  // In your webhook handler with both signatures:
  app.post('/webhook', (req, res) => {
    // Get both signatures
    const fullSignature = req.headers['x-signature-sha256'] as string;
    const compactSignature = req.headers['parcha-signature-compact'] as string;

    const payloadData = req.body;
    const rawPayload = JSON.stringify(req.body);

    // Option 1: Quick verification with compact signature
    if (compactSignature && verifyCompactSignature(payloadData, compactSignature, YOUR_API_SECRET_KEY)) {
      // Lightweight verification passed - process webhook
      const caseId = payloadData.input_payload.id;
      // ... quick processing based on case_id ...
      res.status(200).send('Webhook received');
    }

    // Option 2: Full verification with standard signature
    else if (fullSignature && verifySignature(rawPayload, fullSignature, YOUR_API_SECRET_KEY)) {
      // Full verification passed - process complete payload
      // ... full processing ...
      res.status(200).send('Webhook received and verified');
    }

    else {
      res.status(401).send('Invalid signature');
    }
  });
  ```

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

  function verifyCompactSignature(payloadData, compactSignature, apiSecretKey) {
    /**
     * Verify the Parcha compact signature using only the case ID.
     *
     * @param {Object} payloadData - The parsed JSON payload
     * @param {string} compactSignature - The signature from the parcha-signature-compact header
     * @param {string} apiSecretKey - Your API secret key
     * @returns {boolean} True if signature is valid, false otherwise
     */

    // Extract the case ID from the payload
    const caseId = payloadData?.input_payload?.id;

    if (!caseId) {
      return false;
    }

    // Convert case ID to string and compute signature
    const idString = String(caseId);
    const computedSignature = crypto
      .createHmac('sha256', apiSecretKey)
      .update(idString)
      .digest('base64');

    // Use constant-time comparison to prevent timing attacks
    return crypto.timingSafeEqual(
      Buffer.from(computedSignature),
      Buffer.from(compactSignature)
    );
  }

  // In your webhook handler with both signatures:
  app.post('/webhook', (req, res) => {
    // Get both signatures
    const fullSignature = req.headers['x-signature-sha256'];
    const compactSignature = req.headers['parcha-signature-compact'];

    const payloadData = req.body;
    const rawPayload = JSON.stringify(req.body);

    // Option 1: Quick verification with compact signature
    if (compactSignature && verifyCompactSignature(payloadData, compactSignature, YOUR_API_SECRET_KEY)) {
      // Lightweight verification passed - process webhook
      const caseId = payloadData.input_payload.id;
      // ... quick processing based on case_id ...
      res.status(200).send('Webhook received');
    }

    // Option 2: Full verification with standard signature
    else if (fullSignature && verifySignature(rawPayload, fullSignature, YOUR_API_SECRET_KEY)) {
      // Full verification passed - process complete payload
      // ... full processing ...
      res.status(200).send('Webhook received and verified');
    }

    else {
      res.status(401).send('Invalid signature');
    }
  });
  ```
</CodeGroup>

**Important notes:**

* The compact signature is only present when the payload contains `input_payload.id`
* Both signatures use the same API secret key
* The compact signature signs the **string representation** of the case ID
* Always use constant-time comparison (`hmac.compare_digest` or `crypto.timingSafeEqual`) to prevent timing attacks
* The compact signature is useful for quick validation, but you should still verify the full signature for complete payload integrity

## Testing Your Webhooks

Parcha API provides a `/api/v1/testWebhook` endpoint to help you test both job and tool webhooks. This endpoint sends sample payloads to your specified webhook URLs.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://demo.parcha.ai/api/v1/testWebhook' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "webhook_url": "https://your-webhook-url.com/job-callback",
    "tool_webhook_url": "https://your-webhook-url.com/tool-callback",
    "tool_webhook_ids": ["kyb.incorporation_document_extraction_tool"]
  }'
  ```

  ```python Python theme={null}
  import requests

  api_key = 'YOUR_API_KEY'
  url = 'https://demo.parcha.ai/api/v1/testWebhook'

  data = {
      "webhook_url": "https://your-webhook-url.com/job-callback",
      "tool_webhook_url": "https://your-webhook-url.com/tool-callback",
      "tool_webhook_ids": ["kyb.incorporation_document_extraction_tool"]
  }

  headers = {
      'Authorization': f'Bearer {api_key}',
      'Content-Type': 'application/json'
  }

  response = requests.post(url, json=data, headers=headers)
  print(response.json())  # {"message": "Webhook test successful"}
  ```

  ```typescript TypeScript theme={null}
  import axios from 'axios';

  const apiKey = 'YOUR_API_KEY';
  const url = 'https://demo.parcha.ai/api/v1/testWebhook';

  const data = {
    webhook_url: "https://your-webhook-url.com/job-callback",
    tool_webhook_url: "https://your-webhook-url.com/tool-callback",
    tool_webhook_ids: ["kyb.incorporation_document_extraction_tool"]
  };

  axios.post(url, data, {
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json'
    }
  })
  .then(response => console.log(response.data))  // {"message": "Webhook test successful"}
  .catch(error => console.error('Error:', error));
  ```
</CodeGroup>

The test endpoint will trigger webhooks for a sample job and send appropriate payloads to your specified URLs. You can test:

* Job webhooks by providing a `webhook_url`
* Tool webhooks by providing a `tool_webhook_url`
* Filtered tool webhooks by also including `tool_webhook_ids`

A successful test will return:

```json theme={null}
{
    "message": "Webhook test successful"
}
```

This allows you to verify your webhook implementation before processing real job events.

## Best Practices

1. Always verify the HMAC signature to ensure the authenticity of webhook payloads.
2. Implement proper error handling and retries in your webhook receiver to manage potential network issues or service interruptions.
3. Process webhook payloads asynchronously to avoid blocking your webhook receiver.
4. Store the raw payload for debugging purposes before processing it.
5. Respond quickly to the webhook request (ideally within a few seconds) to avoid timeouts.
6. When using tool webhooks, be prepared to handle events in any order as different tools have different processing times.
7. Consider implementing separate endpoints for job webhooks and tool webhooks to simplify processing logic.

By following these guidelines and using the provided testing tools, you can ensure a robust and secure webhook integration with the Parcha API.
