Receiving 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 the reading — fast path for coding agents
truto webhooks list -o json
/skill truto
What you need
- 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
400andWebhook support is not yet enabled for this integration. - 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.
- A Truto webhook subscribed to the
record:*events, or toall. See Creating a webhook.
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, monday.com or Customer.io.
https://api.truto.one/integrated-account-webhook/7f3c2a1e-5b9d-4e8f-a2c1-3d4e5f6a7b8c
https://api.truto.one/environment-integration-webhook/0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4eSigning 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_secretvariable 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 ascrm/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 ascontact.propertyChange.raw_payload: the event as the vendor sent it.
{
"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.
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
POSTwith a JSON body gets200once 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 a5xxit retries. A per-account URL also accepts an empty body. - A per-account URL answers
GETwith an empty200, for vendors that check a URL before saving it. - A URL whose ID doesn't exist gets
404. A body that isn't JSON gets400. - 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
429withRetry-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 with the unified_webhook:batch trigger. Its steps see the batch's events.
Next
See Event types for every event Truto sends. Each record:* delivery to your endpoint shows under Logs → Webhook; see Viewing logs.