# Webhooks

Apideck pushes a webhook event to your delivery URL the moment data changes in a connected integration,
instead of you polling the API on a schedule. Subscribe via the Apideck admin or the Webhook subscription
API, and Apideck normalizes events across every connector that supports webhook events, including some
that don't natively support webhooks, through Virtual Webhooks.

- [Pull versus Push](#pull-versus-push)
- [Virtual Webhooks](#virtual-webhooks)
- [How to subscribe to webhook events?](#how-to-subscribe-to-webhook-events)
- [How to activate webhook events for integrations?](#how-to-activate-webhook-events-for-integrations)
- [Vault connection events and their payloads](#vault-connection-events-and-their-payloads)

Learn more about [what's a webhook](https://blog.apideck.com/what-is-a-webhook) on our blog.

## Pull versus Push

Polling (pull) means your application repeatedly requests the API on a schedule to check for new data.
Webhooks (push) mean you register a callback URL once, and the API sends a POST request to it the moment a
matching event occurs. Polling gives you control over request timing but adds latency and wasted requests
when nothing has changed; webhooks deliver changes immediately but require your endpoint to be reachable and
capable of validating and processing incoming requests.

### When to use webhooks and when to poll?

Use webhooks when the data is time-sensitive and you need to react as soon as it changes. They're also more
resource-efficient, since your endpoint is only called when an event actually happens.

Use polling when updates aren't time-sensitive, or you need a guaranteed refresh even if nothing changed
(for example, refreshing a user count once an hour regardless of whether it moved).

The use cases for webhooks are numerous. See [Apideck: What is a webhook?](https://blog.apideck.com/what-is-a-webhook)
on our blog for common examples.

## Virtual Webhooks

![Missing native webhooks](/guides/webhooks/virtual-webhooks-connector.png)

Some downstream APIs don't natively support webhooks. For those, Apideck's Virtual Webhooks poll the
downstream API on your behalf and emit the same unified webhook events you'd get from a native webhook,
so your integration code doesn't need to know which connectors support webhooks natively and which don't.

![Virtual webhooks](/guides/webhooks/virtual-webhooks.png)

Apideck's polling engine monitors records inside the downstream API for changes and converts them into
unified webhook events, which is also what powers features like bi-directional sync.

Apideck handles the sync and polling complexity for you, including pagination, automatic retries, rate
limits, and change tracking, without persisting your data on Apideck's side.

## How to subscribe to webhook events?

Subscribe to Unify events either through the Apideck admin dashboard or through the Webhook subscription API.

### Via the Apideck admin

Go to **Configuration > Webhooks** or [https://platform.apideck.com/webhooks](https://platform.apideck.com/webhooks)
to manage existing webhook subscriptions.

![Unify admin - manage webhooks](/guides/webhooks/webhook-admin.png)

Click **Add Webhook** to create a new subscription.

![Unify admin - Register webhook subscription](/guides/webhooks/webhook-subscription.png)

Enter the delivery URL of the webhook endpoint on your system, and select the Unify API to choose which
events you want to receive.

Apideck validates the delivery URL when you create the subscription, and expects it to respond with an HTTP
200 to the validation POST request. See [how webhook delivery URL validation works](https://help.apideck.com/en/articles/4232258)
for details. The validation request carries the header `x-apideck-event-type: apideck.subscription.created`
and this body:

`{"message":"Validating delivery URL"}`

Once you click **Create**, you're subscribed to the Unify webhook events you selected.

### Via the Apideck Webhook subscription API

Manage webhook subscriptions programmatically through the [Webhook API](https://developers.apideck.com/apis/webhook/reference#tag/Webhooks)
or test it live in the [API explorer](https://developers.apideck.com/apis/webhook/api-explorer).

![Developer Docs - Webhooks API](/guides/webhooks/webhook-docs.png)

## How to activate webhook events for integrations?

For Unify to forward webhook events from a specific integration, that integration must support webhook
events, and Unify's endpoint must be registered with it to receive them.

For integrations whose API supports subscription management, Apideck auto-registers the subscription for
you. For integrations that don't support auto-registration, you or the customer activating the integration
must manually enter the Unify endpoint in that integration's own web portal.

For example, with QuickBooks:

![Example - QuickBooks webhooks activation](/guides/webhooks/webhook-activation-1.png)

At the bottom of the connector configuration, copy the unique Unify endpoint shown there and paste it into
the webhook settings in QuickBooks.

Once that's done, Unify receives the webhook events QuickBooks (or whichever integration) sends, converts
them into unified Unify events, and pushes them to the delivery endpoint you configured when registering the
webhook subscription.

## Vault connection events and their payloads

Alongside events from the Unified APIs (new invoice, updated employee, ...), Vault emits its own events whenever something changes about a **connection** itself, rather than the data behind it. Subscribe to these the same way as any other event, [via the Apideck admin or the Webhook subscription API](#how-to-subscribe-to-webhook-events), by selecting the **Vault API** and picking the event you need.

Every Vault connection event delivers the same shape: the full [Connection resource](https://developers.apideck.com/apis/vault/reference#tag/Connections/operation/connectionsOne) as `entity`, plus an `entityType` of `Connection`:

```json
{
  "entity": {
    "id": "accounting+quickbooks",
    "application_id": "dSBdXd2H6Mqwfg0atXHXYcysLJE9qyn1VwBtXHX",
    "consumer_id": "test_consumer_id",
    "service_id": "quickbooks",
    "unified_api": "accounting",
    "auth_type": "oauth2",
    "state": "callable",
    "health": "ok",
    "enabled": true,
    "settings": {},
    "metadata": {},
    "created_at": "2021-09-10T10:39:49.628Z",
    "updated_at": "2021-09-10T10:39:52.715Z"
  },
  "entityType": "Connection"
}
```

Which event you get, and which field on that same payload is the signal to act on, depends on what changed:

- **`vault.connection.callable`**, fires the moment a connection transitions from any other state to `callable`: OAuth scopes were granted (or, for non-OAuth connectors, the entered credentials were validated). Good place to kick off an initial sync.
- **`vault.connection.updated`**, a general-purpose event that fires whenever any connection data changes as a result of a Vault-driven action (adding/updating credentials, an OAuth callback, importing a connection, updating settings). Its payload includes the connection's current `state`, so this is also how you find out a connection became `invalid`, not a dedicated `vault.connection.invalid` event. See the [multi-company accounting guide](/guides/multi-company-accounting#reading-metadatacompany_id) for using this event to detect an organization change via `metadata.company_id`.
- **`vault.connection.disabled`**, fires when a connection's `enabled` property changes from `true` to `false`.
- **`vault.connection.revoked`**, fires when OAuth access is explicitly revoked and that revocation is confirmed at the integration's side.
- **`vault.connection.token_refresh.pending`**, an *early warning*, not a failure signal. Vault refreshes OAuth tokens proactively and retries roughly every 15 minutes if an attempt fails. This fires on the first failed attempt, while the connection stays `callable` and `health` reads `pending_refresh` for up to 48 hours as Vault keeps retrying.
- **`vault.connection.token_refresh.failed`**, the *terminal* event. It fires only once that 48-hour retry window elapses with no successful refresh, at which point credentials are cleared and the connection is no longer callable. Don't treat `token_refresh.pending` as a precursor you can ignore until this fires: by design it can be up to 48 hours later. See the [Migrating Integrations guide](/guides/migrating-integrations#webhooks) for how to enable this event.
- **`vault.connection.downstream.degraded`** / **`vault.connection.downstream.recovered`**, a connection can stay `callable` while the integration itself is temporarily unreachable. These two events cover that separately from `state`.

>
> One case isn't covered by any event above: a request to the integration comes back unauthorized even though the stored credentials didn't look expired or invalid locally, for example access was revoked earlier than its recorded expiry, or the connector uses static (non-OAuth) credentials with no refresh cycle at all. Vault still marks the connection `invalid`, but that specific path doesn't emit a webhook and, since it isn't part of the token-refresh retry cycle, won't self-correct either. Calling [`validateConnectionState`](https://developers.apideck.com/apis/vault/reference#operation/validateConnectionState) yourself forces a fresh check; otherwise, treat a 401/unauthorized response from your own Unified API or Proxy calls as the signal for this specific case.

For the full connection lifecycle these events move through, and what each `state` and `health` value means, see [Reacting to state changes without polling](/guides/connection-states#reacting-to-state-changes-without-polling) in the Vault Connection States guide.
