Guide to ZoomInfo Webhooks: Features and Best Practices
ZoomInfo webhooks keep the B2B contact and company records you've bought through the ZoomInfo Enterprise API up to date. When ZoomInfo changes a record you've enriched, such as a contact's email or job title or a company's revenue, it sends the new data to your application. Without them you would re-enrich records on a schedule to find out what had changed.
This guide covers what ZoomInfo webhooks send, how to set them up in the Admin Portal or through the Monitoring API, how to check the verification token, and the best practices for production.
What are ZoomInfo webhooks?
ZoomInfo webhooks are part of the Monitoring API, which is only available to organisations with the Scaling API. A webhook has a title, a target URL and one or more subscriptions, and each subscription names an object type (Contact or Company) and an event type. Update is the only event type ZoomInfo supports, so a webhook tells you that a record changed, never that one was created or deleted.
Coverage is limited to records you've paid for through the API. You must have enriched at least one contact or company through the API before you can subscribe, you only receive updates for records purchased through the API in the past 12 months, and records enriched from the ZoomInfo platform itself aren't covered.
ZoomInfo doesn't sign the body. Instead, it generates a verification token for each webhook and sends it in the x-zoominfo-token header of every POST. Your application compares the header with the token it stored.
ZoomInfo webhook features
| Feature | Details |
|---|---|
| Availability | Organisations with the Scaling API |
| Configuration | Admin Portal (Integrations > API & Webhooks) or the Monitoring API |
| Object types | Contact and Company |
| Event types | Update only |
| Payload types | Full payload (default) or changed attributes only |
| Batch size | Up to 25 profiles per delivery |
| Authentication | Verification token in the x-zoominfo-token header |
| Signature | None. Nothing is computed over the body |
| Token | Generated by ZoomInfo; regenerating revokes the old one immediately |
| Target URL | Must be https:// |
| Retries, timeout, delivery ID | Not documented |
| Source IPs | Not published |
Common events
Each subscription is an object type plus Update:
| Object type | Event type | Fires when |
|---|---|---|
Contact | Update | A contact you enriched through the API in the past 12 months changes |
Company | Update | A company you enriched through the API in the past 12 months changes |
There's no event name string in the body or headers. Each delivery describes its subscription in webhookDetails and carries the changed records in a data array. Here is ZoomInfo's example of a company update with changed attributes only:
{
"webhookDetails": {
"id": "xxxx",
"title": "webhook-title",
"objectType": "company",
"eventType": "Update"
},
"data": [
{
"id": 344589814,
"name": "ZoomInfo",
"website": "www.zoominfo.com",
"revenue": 1210000,
"employeeCount": 3500,
"changedAttributes": ["revenue", "employeeCount"]
}
]
}
Every profile includes changedAttributes, the list of fields that changed, and the fields you need to match it to your own records: id, firstName, lastName and email for contacts, and id, name and website for companies. A full payload adds every other output field for the record.
Note the case of objectType. ZoomInfo's guide, its subscription types and its contact examples use Contact and Company, but both company examples send company.
Setting up ZoomInfo webhooks
In the Admin Portal:
- Go to Admin Portal > Integrations > API & Webhooks and, under Webhooks, click Create New.
- Enter a Webhook Title and the Target URL.
- Click Validate. ZoomInfo sends a sample JSON payload to the URL, and you check that it arrived.
- Click Generate New Token, then Copy Token or Download Token. Store it, for example as
ZOOMINFO_WEBHOOK_TOKEN. - Click Create Subscription, pick the object type, the event and the payload type (full by default, or toggle on changed attributes only), and save. Add more subscriptions if you need them, then save the webhook.
- Once your application checks the token, open Edit Webhook, switch Activation Status to ON and save. The webhook shows as Active.
Through the Monitoring API, create the webhook disabled, as ZoomInfo recommends:
curl -X POST "https://api.zoominfo.com/webhooks" \
-H "Authorization: Bearer $ZOOMINFO_JWT" \
-H "Content-Type: application/json" \
-d '{
"title": "My Webhook",
"enabled": false,
"targetUrl": "https://example.com/webhooks/zoominfo",
"subscriptions": [
{ "eventType": "Update", "objectType": "Contact", "fullPayload": true },
{ "eventType": "Update", "objectType": "Company", "fullPayload": false }
]
}'
The response includes the webhook id and its verificationToken. Check the URL with POST /webhooks/validate, store the token in your application, then enable the webhook with PUT /webhooks/{id} and { "enabled": true }.
For local development, hookdeck listen 3000 zoominfo --path /webhooks/zoominfo gives you a public HTTPS URL that forwards to your machine.
Securing ZoomInfo webhooks
Read x-zoominfo-token and compare it with your stored token in constant time. Since there's no HMAC, you don't need the raw body for verification, but reading it raw does no harm and keeps the handler consistent with signed providers.
const crypto = require('crypto');
const express = require('express');
function verifyZoomInfoToken(tokenHeader, expectedToken) {
if (!tokenHeader || !expectedToken) return false;
const received = Buffer.from(tokenHeader, 'utf8');
const expected = Buffer.from(expectedToken, 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
const app = express();
app.post('/webhooks/zoominfo', express.raw({ type: '*/*' }), (req, res) => {
const token = process.env.ZOOMINFO_WEBHOOK_TOKEN;
if (!token) return res.sendStatus(500); // fail closed once the webhook is active
if (!verifyZoomInfoToken(req.get('x-zoominfo-token'), token)) {
return res.sendStatus(401);
}
const payload = JSON.parse(req.body.toString('utf8'));
const object_type = payload.webhookDetails.objectType.toLowerCase(); // 'contact' or 'company'
// Queue payload.data (up to 25 profiles) for processing, then acknowledge.
res.sendStatus(200);
});
The same check in Python:
import hmac
def verify_zoominfo_token(token_header, expected_token) -> bool:
if not token_header or not expected_token:
return False
return hmac.compare_digest(token_header.encode("utf-8"), expected_token.encode("utf-8"))
# In FastAPI: verify_zoominfo_token(request.headers.get("x-zoominfo-token"),
# os.environ["ZOOMINFO_WEBHOOK_TOKEN"])
A static token proves the request came from someone who knows the token. It doesn't protect the body, and it gives no protection against replays, so serve the endpoint over HTTPS only, which ZoomInfo requires anyway, and keep the token out of logs.
Make ZoomInfo webhooks production-ready. Hookdeck Event Gateway checks
x-zoominfo-tokenbefore anything reaches your code, and durably queues every contact and company update for your service.
ZoomInfo webhook limitations and pain points
No signature, only a static token
The Problem: Anyone who obtains the token can send forged updates, and a captured request can be replayed indefinitely.
Why It Happens: ZoomInfo calls it a "signature token", but it sends the token itself in the header. Nothing is computed over the body, there's no timestamp, and ZoomInfo doesn't publish source IPs.
Workarounds:
- Compare the token in constant time, and only accept requests over HTTPS.
- Rotate the token periodically, as ZoomInfo suggests.
- Treat each update as an upsert of the record's current values, so a replayed request doesn't do harm.
How Hookdeck Can Help: Hookdeck's ZoomInfo source type compares x-zoominfo-token with the token you enter and stops requests that fail before they reach your service.
The Validate step comes before the token
The Problem: Validation fails, or your endpoint rejects ZoomInfo's sample, even though the URL is correct.
Why It Happens: In the Admin Portal, you click Validate before you generate the token, and the POST /webhooks/validate endpoint takes only a URL and an object type, not a webhook ID. ZoomInfo doesn't say whether the sample carries x-zoominfo-token, so an endpoint that already requires the token may reject it.
Workarounds:
- Accept the sample without processing it while your application has no token configured, then require the token before you activate the webhook.
- If you re-validate later and get a
401, check whether the sample carried the header.
How Hookdeck Can Help: While the Hookdeck source has no token configured, Hookdeck accepts the Validate sample and logs it, so you can see exactly what it contained. Add the token to the source after validating and before you activate the webhook.
Token rotation has no overlap
The Problem: Deliveries start failing the moment you rotate the token.
Why It Happens: Generating a new verification token revokes the previous one immediately. ZoomInfo keeps sending, and every delivery fails the token check until your application has the new value.
Workarounds:
- Rotate during a quiet period, and update the stored token straight after generating the new one.
- Keep the token in one place, such as a secrets manager, so the update is a single change rather than a redeploy of several services.
How Hookdeck Can Help: When Hookdeck verifies the token and stops requests that fail before they reach your service, your services never need it. Rotation becomes one change: update the token on the Hookdeck source as soon as you generate it in ZoomInfo.
Retries, timeouts and delivery IDs are undocumented
The Problem: You don't know whether ZoomInfo will resend an update your endpoint missed, or how long you have to respond.
Why It Happens: ZoomInfo's guide and API reference don't describe a retry policy, a timeout, a success criterion or a delivery ID, and webhookDetails.id identifies the webhook, not the delivery.
Workarounds:
- Respond
2xxquickly and process in the background. - Keep your endpoint highly available, and re-enrich records through the API if you suspect you've missed updates.
How Hookdeck Can Help: Hookdeck accepts and stores every ZoomInfo delivery even when your service is down, then retries delivery to your service and lets you replay anything that failed.
Up to 25 profiles per request
The Problem: One request carries many records, and one bad record can fail the whole batch.
Why It Happens: ZoomInfo batches up to 25 profiles into the data array of a single delivery.
Workarounds:
- Process each profile independently, and record failures per profile rather than failing the request.
- Match each profile to your records by its ZoomInfo
id.
How Hookdeck Can Help: A Hookdeck transformation can reshape the batch before delivery, and rate limiting keeps a burst of updates from overwhelming your CRM sync.
Best practices
Compare the object type case-insensitively
Lower-case webhookDetails.objectType before you route on it. ZoomInfo's own examples send both Company and company, and an exact match on Company could drop every company update.
Use changedAttributes to limit writes
Update only the fields listed in changedAttributes, even with a full payload. It saves writes to your CRM and avoids overwriting fields you've edited locally.
Create webhooks disabled
When you use the Monitoring API, create the webhook with enabled: false, validate the URL, store the token, and only then enable it, as ZoomInfo recommends.
Make processing idempotent
With no delivery ID, write each update as "set this record to these values" so that applying it twice gives the same result. See how to implement webhook idempotency.
Acknowledge fast and process asynchronously
Check the token, store the payload, return 200, and process the profiles in the background. See why to process webhooks asynchronously.
Conclusion
ZoomInfo webhooks send contact and company updates for records you've enriched through the API, authenticated by a static verification token in x-zoominfo-token rather than a signature. Compare the token in constant time, accept the Validate sample before you've stored the token, plan token rotation carefully, route on objectType case-insensitively, and process each of the up to 25 profiles on its own.
Hookdeck Event Gateway checks every ZoomInfo token, accepts updates even while your service is down, and holds each one until your service has processed it.
Get started with Hookdeck for free and handle ZoomInfo webhooks reliably in minutes.