# Webhooks

Subscribe to events and receive signed HTTP notifications.

Source: <https://docs.finbar.com/api-mcp/webhooks>

Webhooks send an HTTP request to your application when an event occurs in Finbar.
They are available to paid accounts with public API access. The only supported
event type is currently `document.published`.

Subscriptions belong to your billing account. Authorized users and API clients
on that account can manage them. Each account can have up to 1,000 subscriptions.
Delivery has no additional charge and does not use your document-read quota.

## Create a subscription

Use an access token with the `webhooks:manage` and `documents:read` scopes.
If your credential lacks either scope, create a replacement REST credential;
rotating a credential does not add scopes.

Provide the entity to watch and the URL that will receive events. Each
subscription currently receives `document.published` events for one entity.

```bash
curl --fail-with-body --silent --show-error \
  'https://api.finbar.com/v1/webhooks' \
  --header "Authorization: Bearer $ACCESS_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{"entity_id":"O-YOUR_ENTITY_ID","url":"https://your.example/finbar-events"}'
```

The `201` response contains `id`, `entity_id`, `url`, `created_at`, and
`signing_secret`. Save the signing secret when you create the subscription; it
cannot be retrieved later. It is a base64-encoded 32-byte key. Keep it out of
logs, source control, and callback URLs.

To change the callback URL or replace a lost secret, delete the subscription
and create another. Creation does not support `Idempotency-Key`. If the response
is lost, list existing subscriptions before trying again.

The callback must use public HTTPS on port 443. Private addresses, embedded URL
credentials, and URL fragments are rejected. Finbar does not follow redirects.

## Event types

### `document.published`

Sent when a document first finishes processing and is ready to read. The
subscription covers every document type for the selected entity. Documents
already ready when you subscribe are excluded. Reprocessing and metadata
corrections do not produce another event.

Email alerts and webhooks both exclude documents with a reporting year before
2023, or at least two years behind the latest available document for that
entity. For example, if the latest reporting year is 2026, documents from 2024
and earlier do not trigger notifications. The comparison includes all document
types and uses the reporting period year, falling back to the report date when
no period year is set. Documents without a usable year are excluded.

This check also applies before delivery retries. A queued notification can be
cancelled if newer documents make it too old.

Example payload:

```json
{
  "id": "962410e0-738c-4479-b90b-85674cc3b350",
  "type": "document.published",
  "occurred_at": "2026-10-28T12:00:00Z",
  "subscription_id": "53948a28-5a7b-4396-84b1-3e4b87d36013",
  "document": {
    "document_id": "D-YOUR_DOCUMENT_ID",
    "entity_id": "O-YOUR_ENTITY_ID",
    "period": {"year": 2026, "period_type": "Q", "period": 3},
    "publish_date": "2026-10-28T00:00:00Z",
    "report_date": "2026-10-28T00:00:00Z",
    "status": "PARSED",
    "type": "FULL_FINANCIAL_REPORT"
  }
}
```

The `document` object uses the same metadata format as the document list API.
Use `document_id` to [read the document](https://docs.finbar.com/api-mcp/documents.md). The event contains
metadata only, without the document text or storage URLs.

## Verify signatures

Requests include:

- `Webhook-Id`: the event ID, unchanged across retries.
- `Webhook-Timestamp`: Unix seconds for this delivery attempt.
- `Webhook-Signature`: `v1=` followed by a hexadecimal HMAC-SHA256 digest.

Base64-decode the signing secret and use it as the HMAC key. Sign the exact bytes
of `Webhook-Id + "." + Webhook-Timestamp + "." + raw_request_body`.
Use the unmodified request body, before parsing JSON. Compare signatures in
constant time and reject timestamps more than five minutes from your clock.
Keep clocks synchronized. Each retry has a fresh timestamp and signature.

```python
import base64
import hashlib
import hmac
import time

def verify(secret, event_id, timestamp, signature, raw_body):
    try:
        if abs(time.time() - int(timestamp)) > 300:
            return False
        message = f"{event_id}.{timestamp}.".encode() + raw_body
        expected = "v1=" + hmac.new(
            base64.b64decode(secret, validate=True), message, hashlib.sha256
        ).hexdigest()
        return hmac.compare_digest(expected, signature)
    except (ValueError, TypeError):
        return False
```

## Delivery and retries

Finbar aims to start delivery within a few seconds of an event. Events may arrive
out of order or more than once. Check the signature, save the event to a durable
queue, and return a `2xx` response. Process the queued event separately. Use
`(subscription_id, id)` to avoid processing the same event twice.

Requests time out after 10 seconds. Finbar retries network failures and
non-`2xx` responses for up to 24 hours after queueing the delivery. Retry delays
increase to a maximum of one hour and vary to spread requests over time. When
the retry period ends, the delivery is marked failed. Later events are still
sent. There is no manual replay endpoint.

## Manage subscriptions

Management endpoints require `webhooks:manage`. Any authorized user or client
on the billing account can list, inspect, or delete its subscriptions and read
their delivery history.

| Method and path                            | Purpose                             |
| ------------------------------------------ | ----------------------------------- |
| `GET /v1/webhooks`                         | List account subscriptions          |
| `GET /v1/webhooks/{webhook_id}`            | Inspect a subscription              |
| `DELETE /v1/webhooks/{webhook_id}`         | Stop future and queued deliveries   |
| `GET /v1/webhooks/{webhook_id}/deliveries` | Inspect 30 days of delivery history |

Lists return `items` and, when another page is available, `next_cursor`. Pass
that value as `cursor` to fetch the next page. The `limit` parameter defaults to
50 and accepts values from 1 to 100. Management requests share an account limit
of 60 per minute, with a burst of 10.

Delivery history includes status, attempt count, timestamps, the last HTTP
status, and a failure code. It excludes response bodies and signing secrets.
Delivered, failed, and cancelled delivery records are removed once they are
30 days old.

## Stop delivery

Delete a subscription to stop an integration. Rotating or revoking the
credential that created it does not stop delivery. Subscriptions also continue
if the creating user leaves the team.

Delivery stops if the account loses its paid plan or public API access. When
access is restored, delivery resumes for future events; events from the
interruption are not replayed. Accounts that retain API access can still
inspect and delete subscriptions after a downgrade.

A request already in progress may complete after a subscription is deleted or
account access is lost.
