# Receiving vendor webhooks

> Source: https://truto.one/docs/guides/webhooks/inbound-vendor-webhooks/

When a contact changes in HubSpot or an employee joins in HiBob, the vendor can tell Truto, and Truto tells you with a `record:updated` or `record:created` event in the same shape as the Unified API. This page covers the URLs to paste into vendor consoles and what arrives at your endpoint.

:::skip-reading{cli="truto webhooks list -o json" skill="truto"}
:::

## What you need

1. **An integration whose configuration handles webhooks.** Its integration guide says how to set them up. If the integration has none, Truto answers the vendor with `400` and `Webhook support is not yet enabled for this integration.`
2. **A Unified API installed in the environment whose model maps that integration's webhooks**, such as CRM for HubSpot or HRIS for HiBob. The mapping turns each vendor event into a unified one. Without one, Truto accepts the vendor's request and sends you nothing.
3. **A Truto webhook subscribed to the `record:*` events**, or to `all`. See [Creating a webhook](/docs/guides/webhooks/creating-a-webhook-through-ui).

## Pick the URL

Truto has two receiving URLs. Which one a vendor needs depends on how it sends webhooks, and the integration guide names it.

| URL | Use it when |
|---|---|
| `https://api.truto.one/integrated-account-webhook/<integrated_account_id>` | The vendor lets each customer register their own webhook, so every connected account gets its own URL. |
| `https://api.truto.one/environment-integration-webhook/<environment_integration_id>` | The vendor sends every customer's events to one URL registered on your app, such as a HubSpot or monday.com developer app. Truto works out which accounts each event belongs to. |

Find an `integrated_account_id` on the account's page under **Integrated Accounts**. Find an `environment_integration_id` under **Integrations → Installed**, or with `GET /environment-integration?integration_name=<name>`.

Some integrations register the per-account webhook with the vendor for you when an account connects. For the rest, register the URL in the vendor's console as its integration guide describes, such as [HubSpot](/docs/integration-guides/hubspot), [monday.com](/docs/integration-guides/mondaycom) or [Customer.io](/docs/integration-guides/customerio/registering-webhooks).

```text
https://api.truto.one/integrated-account-webhook/7f3c2a1e-5b9d-4e8f-a2c1-3d4e5f6a7b8c
https://api.truto.one/environment-integration-webhook/0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e
```

## Signing secrets

Most vendors sign their webhooks, and Truto checks the signature before it does anything else. A request that fails the check gets `400` with `Event signature verification failed.` Where Truto finds the secret depends on the integration:

- **Per account**: in the account's context. For example, Customer.io's signing key goes in a `webhook_secret` variable on the account (**Edit variables** on the account's page).
- **Per installed integration**: in the installed integration's settings, such as the OAuth client secret of your own app, or a value under **Environment variables**.

The integration guide names the field. Truto also answers the handshake some vendors send when you register a URL, such as a challenge to echo back, without passing it on to you.

## What arrives at your endpoint

For each vendor event the mapping recognises, Truto sends one `record:*` event to your subscribed webhooks:

| Event | Sent when |
|---|---|
| `record:created` | A record was created in the vendor. |
| `record:updated` | A record was changed. |
| `record:deleted` | A record was deleted. |
| `record:meta` | The vendor sent something that isn't about a record, such as a subscription lifecycle notice. `records` is empty. |

When the vendor sends only an ID, Truto fetches the full record from the vendor through the Unified API first, so `records` holds the same unified shape a `GET` returns. The event's `payload` carries:

- `resource`: the unified resource, such as `crm/contacts`.
- `records`: the record, as an array of one.
- `integrated_account_id`: the account the event belongs to.
- `raw_event_type`: the vendor's own event name, such as `contact.propertyChange`.
- `raw_payload`: the event as the vendor sent it.

```json
{
  "id": "5d1f3c8a-0b2e-4c6d-9a7f-1e2d3c4b5a69",
  "event": "record:updated",
  "payload": {
    "resource": "crm/contacts",
    "records": [
      {
        "id": "901",
        "first_name": "Ada",
        "last_name": "Lovelace",
        "emails": [{ "email": "ada@example.com", "is_primary": true }]
      }
    ],
    "integrated_account_id": "7f3c2a1e-5b9d-4e8f-a2c1-3d4e5f6a7b8c",
    "raw_event_type": "contact.propertyChange",
    "raw_payload": { "objectId": 901, "subscriptionType": "contact.propertyChange" }
  },
  "environment_id": "ac15abdc-b38e-47d0-97a2-69194017c177",
  "created_at": "2026-11-02T09:21:22.369Z"
}
```

Verify the `x-truto-signature` header as for any Truto webhook. See [Processing webhook events](/docs/guides/webhooks/processing-webhook-events).

If fetching the full record fails because the account's credentials no longer work, Truto marks the account `needs_reauth` and you get `integrated_account:authentication_error`.

## How Truto answers the vendor

- A `POST` with a JSON body gets `200` once Truto has checked and stored it, on both per-account and per-environment URLs. Truto maps and delivers it afterwards, so a slow endpoint of yours doesn't hold up the vendor's request. If storing it fails, Truto retries before answering, so during an outage the vendor's request can take several seconds and may end in a `5xx` it retries. A per-account URL also accepts an empty body.
- A per-account URL answers `GET` with an empty `200`, for vendors that check a URL before saving it.
- A URL whose ID doesn't exist gets `404`. A body that isn't JSON gets `400`.
- On a per-account URL, Truto collects events into batches of up to 10, and waits up to 10 seconds for a batch to fill. Expect the `record:*` event up to about 10 seconds after the vendor sends its webhook, plus the time to fetch and map the record.
- Each URL takes up to 300 requests every 10 seconds. Past that, Truto answers `429` with `Retry-After: 10`, and most vendors retry.

## Run a sync job on vendor events

To act on a batch of these events inside Truto, such as starting a sync job, create a [workflow](/docs/guides/workflows/overview) with the `unified_webhook:batch` trigger. Its steps see the batch's `events`.

## Next

See [Event types](/docs/guides/webhooks/list-webhook-events) for every event Truto sends. Each `record:*` delivery to your endpoint shows under **Logs → Webhook**; see [Viewing logs](/docs/guides/logs/viewing-logs).
