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

# Webhooks and callbacks

> Stream Matcher events to external tools and receive resolution callbacks to keep your reconciliation workflows in sync.

Webhooks enable real-time communication between Matcher and external systems. This guide covers outbound event notifications and inbound resolution callbacks.

## Overview

***

Matcher supports bidirectional webhook communication, keeping your operational tools in sync with every reconciliation event in real time. This reduces manual intervention, helps maintain SLA compliance, and ensures a continuous audit trail across all connected systems.

* **Outbound webhooks**: Matcher notifies external systems when events occur
* **Inbound callbacks**: External systems notify Matcher when actions are taken

When something happens in Matcher, like a new exception or a completed match, it pushes an event to your configured endpoints. External systems like JIRA or ServiceNow can then send callbacks to update exception status or close items automatically. This two-way flow keeps your tools in sync without manual intervention.

<Frame caption="Two-way flow between Matcher and external systems.">
  <img src="https://mintcdn.com/lerian-49cb71fc/UsUgoha8b-OkDZPH/images/en/docs/matcher-webhooks-callbacks.jpg?fit=max&auto=format&n=UsUgoha8b-OkDZPH&q=85&s=126673508391f2166cb220e80901cac5" alt="Matcher Webhooks Callbacks" width="1329" height="1175" data-path="images/en/docs/matcher-webhooks-callbacks.jpg" />
</Frame>

## Outbound events

***

Matcher emits events when significant actions occur in the reconciliation process.

### Available events

| Event                   | Trigger                      | Typical Use                      |
| ----------------------- | ---------------------------- | -------------------------------- |
| `ingestion.completed`   | File import finished         | Data pipeline monitoring         |
| `ingestion.failed`      | File import failed           | Error alerting                   |
| `match.created`         | New match proposed           | Audit logging                    |
| `match.confirmed`       | Match approved               | Downstream updates               |
| `match.rejected`        | Match declined               | Investigation triggers           |
| `match.force_matched`   | Manual force match applied   | Audit trail, approval workflows  |
| `match.unmatched`       | Match manually reversed      | Audit trail, correction tracking |
| `match_run.started`     | Automatic match job started  | Job monitoring                   |
| `match_run.completed`   | Automatic match job finished | Job monitoring, reporting        |
| `match_run.failed`      | Automatic match job failed   | Error alerting                   |
| `exception.created`     | New exception                | Ticket creation                  |
| `exception.assigned`    | Exception assigned           | User notification                |
| `exception.escalated`   | Exception escalated          | Management alerts                |
| `exception.resolved`    | Exception resolved           | Status sync                      |
| `exception.sla_warning` | SLA approaching              | Proactive alerts                 |
| `exception.sla_breach`  | SLA exceeded                 | Escalation triggers              |
| `context.closed`        | Reconciliation period closed | Reporting triggers               |

### Event payload structure

All events follow a consistent structure:

```json theme={null}
{
 "id": "evt_001",
 "type": "exception.created",
 "timestamp": "2024-01-20T10:30:00Z",
 "version": "1.0",
 "tenant_id": "tenant_001",
 "context_id": "ctx_abc123",
 "data": {
 // Event-specific data
 },
 "metadata": {
 "correlation_id": "req_xyz789",
 "source": "matching_engine"
 }
}
```

## Event payloads

***

### Ingestioncompleted

Fired when a batch of transactions is successfully imported.

```json theme={null}
{
  "id": "evt_ing_001",
  "type": "ingestion.completed",
  "timestamp": "2024-01-20T10:30:00Z",
  "data": {
    "job_id": "job_imp_001",
    "source_id": "src_bank456",
    "source_name": "Chase Bank",
    "file_name": "statement_january.csv",
    "summary": {
      "total_rows": 1250,
      "imported": 1240,
      "duplicates_skipped": 8,
      "validation_errors": 2
    },
    "duration_seconds": 45,
    "started_at": "2024-01-20T10:29:15Z",
    "completed_at": "2024-01-20T10:30:00Z"
  }
}
```

### Matchconfirmed

Fired when a match is approved (auto or manual).

```json theme={null}
{
  "id": "evt_mtch_001",
  "type": "match.confirmed",
  "timestamp": "2024-01-20T10:30:01Z",
  "data": {
    "match_id": "match_001",
    "confidence": 100,
    "rule_id": "rule_exact001",
    "rule_name": "Exact Match",
    "confirmation_type": "AUTO",
    "transactions": [
      {
        "id": "txn_bank_001",
        "source_name": "Chase Bank",
        "amount": 1000.0,
        "currency": "USD",
        "date": "2024-01-15"
      },
      {
        "id": "txn_ledger_001",
        "source_name": "Main Ledger",
        "amount": 1000.0,
        "currency": "USD",
        "date": "2024-01-15"
      }
    ],
    "variance": {
      "amount_diff": 0,
      "date_diff_days": 0
    }
  }
}
```

### Exceptioncreated

Fired when a new exception is identified.

```json theme={null}
{
  "id": "evt_exc_001",
  "type": "exception.created",
  "timestamp": "2024-01-20T10:30:00Z",
  "data": {
    "exception_id": "exc_001",
    "severity": "HIGH",
    "reason": "NO_MATCH_FOUND",
    "reason_details": "No matching transaction found within tolerance",
    "transaction": {
      "id": "txn_bank_999",
      "source_id": "src_bank456",
      "source_name": "Chase Bank",
      "external_id": "BANK-2024-999",
      "amount": 15000.0,
      "currency": "USD",
      "date": "2024-01-15",
      "reference": "Wire transfer #7890"
    },
    "candidates": [
      {
        "transaction_id": "txn_ledger_777",
        "confidence": 55,
        "rejection_reason": "Below 60% threshold"
      }
    ],
    "sla": {
      "due_at": "2024-01-21T10:30:00Z",
      "hours_remaining": 24
    }
  }
}
```

### Exceptionresolved

Fired when an exception is resolved.

```json theme={null}
{
  "id": "evt_exc_002",
  "type": "exception.resolved",
  "timestamp": "2024-01-20T14:30:00Z",
  "data": {
    "exception_id": "exc_001",
    "resolution": {
      "type": "FORCE_MATCH",
      "match_id": "match_forced_001",
      "resolved_by": "user_123",
      "notes": "Verified: amount difference is bank wire fee"
    },
    "time_to_resolution": {
      "hours": 4,
      "within_sla": true
    }
  }
}
```

### Matchforcematched

Fired when a user manually forces a match between transactions.

```json theme={null}
{
  "id": "evt_force_001",
  "type": "match.force_matched",
  "timestamp": "2024-01-20T14:30:00Z",
  "data": {
    "match_id": "match_forced_001",
    "exception_id": "exc_001",
    "forced_by": "user_123",
    "reason": "Amount difference is documented bank fee",
    "notes": "Verified with bank statement showing $250 wire fee deducted",
    "transactions": [
      {
        "id": "txn_bank_999",
        "source_name": "Chase Bank",
        "amount": 15000.0,
        "currency": "USD",
        "date": "2024-01-15"
      },
      {
        "id": "txn_ledger_777",
        "source_name": "Main Ledger",
        "amount": 15250.0,
        "currency": "USD",
        "date": "2024-01-14"
      }
    ],
    "variance": {
      "amount_diff": 250.0,
      "amount_variance_percent": 1.67,
      "date_diff_days": 1
    },
    "requires_approval": true,
    "approval_threshold": 10000.0
  }
}
```

<Note>
  Force match events are commonly used to trigger approval workflows when the variance exceeds company thresholds.
</Note>

### Matchunmatched

Fired when a previously confirmed match is reversed.

```json theme={null}
{
  "id": "evt_unmatch_001",
  "type": "match.unmatched",
  "timestamp": "2024-01-20T15:45:00Z",
  "data": {
    "match_id": "match_001",
    "unmatched_by": "user_456",
    "reason": "Incorrect match - transactions belong to different invoices",
    "notes": "Bank txn is for Invoice #1234, ledger txn is for Invoice #5678",
    "original_match": {
      "confirmed_at": "2024-01-15T10:00:00Z",
      "confirmed_by": "system",
      "confidence": 85,
      "rule_name": "Tolerance Match"
    },
    "transactions": [
      {
        "id": "txn_bank_001",
        "source_name": "Chase Bank",
        "amount": 1000.0,
        "currency": "USD"
      },
      {
        "id": "txn_ledger_001",
        "source_name": "Main Ledger",
        "amount": 1005.0,
        "currency": "USD"
      }
    ],
    "action": {
      "exceptions_created": [
        "exc_new_001",
        "exc_new_002"
      ],
      "status": "Both transactions returned to unmatched pool"
    }
  }
}
```

<Warning>
  Unmatching a confirmed match creates new exceptions for both transactions and triggers a full audit log entry.
</Warning>

### Matchruncompleted

Fired when an automatic or manual match run finishes.

```json theme={null}
{
  "id": "evt_run_001",
  "type": "match_run.completed",
  "timestamp": "2024-01-20T06:05:23Z",
  "data": {
    "run_id": "run_001",
    "context_id": "ctx_abc123",
    "context_name": "Daily Bank Reconciliation",
    "trigger": "SCHEDULED",
    "started_at": "2024-01-20T06:00:00Z",
    "completed_at": "2024-01-20T06:05:23Z",
    "duration_seconds": 323,
    "statistics": {
      "transactions_processed": 1250,
      "matches_found": 1180,
      "matches_confirmed": 1120,
      "matches_pending_review": 60,
      "exceptions_created": 70,
      "by_confidence": {
        "high_90_100": 1120,
        "medium_60_89": 60,
        "low_0_59": 0
      }
    },
    "performance": {
      "transactions_per_second": 3.87,
      "avg_rule_evaluation_ms": 12.5
    }
  }
}
```

### Slawarning / slabreach

Fired when SLA thresholds are reached.

```json theme={null}
{
  "id": "evt_sla_001",
  "type": "exception.sla_warning",
  "timestamp": "2024-01-20T18:30:00Z",
  "data": {
    "exception_id": "exc_001",
    "severity": "HIGH",
    "sla": {
      "due_at": "2024-01-21T10:30:00Z",
      "hours_remaining": 16,
      "percent_remaining": 25
    },
    "assigned_to": "user_123",
    "escalation_path": [
      "user_manager",
      "user_director"
    ]
  }
}
```

## Inbound callbacks

***

External systems send callbacks to Matcher to update exception status after processing. The callback endpoint accepts status updates, resolution notes, and assignee changes from any external system.

### Process a callback

```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/callback" \
 -H "Authorization: Bearer $TOKEN" \
 -H "X-Idempotency-Key: callback-jira-1234" \
 -H "Content-Type: application/json" \
 -d '{
   "externalSystem": "JIRA",
   "externalIssueId": "RECON-1234",
   "status": "RESOLVED",
   "resolutionNotes": "Verified: amount difference is expected bank wire fee",
   "assignee": "john.doe@company.com"
 }'
```

The `externalSystem` field identifies the external system that processed the exception. Common values include `"JIRA"`, `"SERVICENOW"`, or `"WEBHOOK"`, but callbacks can report any system identifier.

#### Response

```json theme={null}
{
  "status": "accepted"
}
```

<Tip>API Reference: [Process callback](/en/reference/matcher/process-exception-callback)</Tip>

When Matcher processes a callback, it updates the exception status and records the resolution in the audit trail. Use the `X-Idempotency-Key` header to prevent duplicate processing.

### Automatic retry for failed callbacks

If a previous callback for the same idempotency key failed during processing, Matcher automatically attempts to reacquire the idempotency lock and reprocess the callback. This means you don't need to generate a new idempotency key when retrying a failed callback — simply resend the same request and Matcher handles the recovery.

The retry behavior applies only to callbacks that were marked as `failed` internally. Callbacks that completed successfully are still deduplicated as expected.

## Webhook security

***

### Signature verification

All webhook deliveries include a signature header for verification:

```
X-Matcher-Signature: sha256=abc123...
X-Matcher-Timestamp: 1705758600
```

**Verification process:**

1. Concatenate timestamp and body: `{timestamp}.{body}`
2. Compute HMAC-SHA256 using webhook secret
3. Compare with signature header

**Example (Node.js):**

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

function verifyWebhook(payload, signature, timestamp, secret) {
 const signedPayload = `${timestamp}.${payload}`;
 const expectedSignature = crypto
 .createHmac('sha256', secret)
 .update(signedPayload)
 .digest('hex');

 return `sha256=${expectedSignature}` === signature;
}
```

**Example (Python):**

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

def verify_webhook(payload, signature, timestamp, secret):
 signed_payload = f"{timestamp}.{payload}"
 expected = hmac.new(
 secret.encode(),
 signed_payload.encode(),
 hashlib.sha256
 ).hexdigest()
 return f"sha256={expected}" == signature
```

### Ip allowlisting

Matcher sends webhooks from specific IP ranges:

```
52.1.2.0/24
52.1.3.0/24
```

Configure your firewall to allow these ranges.

### TLS requirements

* Minimum TLS 1.2
* Valid SSL certificate required
* Self-signed certificates not supported in production

## Retry logic

***

Failed webhook deliveries are retried with exponential backoff.

### Default retry policy

| Attempt | Delay      | Total Wait |
| ------- | ---------- | ---------- |
| 1       | Immediate  | 0s         |
| 2       | 5 seconds  | 5s         |
| 3       | 30 seconds | 35s        |
| 4       | 2 minutes  | 2m 35s     |
| 5       | 10 minutes | 12m 35s    |
| 6       | 1 hour     | 1h 12m 35s |

### Custom retry configuration

```json theme={null}
{
  "retry": {
    "max_attempts": 5,
    "initial_delay_seconds": 10,
    "max_delay_seconds": 3600,
    "backoff_multiplier": 2
  }
}
```

### Retry conditions

Retries occur for:

* HTTP 5xx responses
* Connection timeouts
* DNS resolution failures

No retry for:

* HTTP 4xx responses (except 429)
* Invalid SSL certificates
* Webhook disabled

## Best practices

***

<AccordionGroup>
  <Accordion title="Verify webhook signatures">
    Always verify the HMAC signature before processing webhook payloads. This prevents spoofed requests.
  </Accordion>

  <Accordion title="Respond quickly">
    Return a 2xx response within 5 seconds. Process the event asynchronously if needed.
  </Accordion>

  <Accordion title="Handle duplicates idempotently">
    Events may be delivered more than once. Use the event ID to deduplicate.
  </Accordion>

  <Accordion title="Monitor delivery health">
    Set up alerts for webhook failure rates. Investigate persistent failures promptly.
  </Accordion>

  <Accordion title="Use event filters">
    Subscribe only to events you need. Filtering reduces noise and processing load.
  </Accordion>

  <Accordion title="Test before production">
    Use the test endpoint to verify your webhook handler works correctly before enabling in production.
  </Accordion>
</AccordionGroup>

## Next steps

***

<CardGroup cols={2}>
  <Card title="Exception Routing" icon="route" href="/en/matcher/configuration/matcher-exception-routing">
    Configure how exceptions trigger webhook events.
  </Card>

  <Card title="External Sources" icon="building-columns" href="/en/matcher/integrations/matcher-external-sources">
    Set up data sources that can push via webhooks.
  </Card>
</CardGroup>
