# Guide to Autotask Webhooks: Features and Best Practices

Autotask webhooks tell your integration when companies, contacts, assets, tickets and ticket notes change in Autotask PSA, the professional services automation suite that many managed service providers run on. Autotask now comes from Datto, part of Kaseya. If you build a CRM sync, a quoting tool or a middleware connector for Autotask, webhooks replace repeated polling of the API for changes.

This guide covers how Autotask webhooks are created through the API, the callout payload, how the HMAC-SHA1 signature works, and the limits that shape a production integration.

## What are Autotask webhooks?

Autotask webhooks exist only in the API. There is no form in the Autotask UI to create one, just a read-only list for administrators. You create a webhook on one entity's webhooks endpoint, such as `CompanyWebhooks` or `TicketWebhooks`, and choose which fields trigger it and which are always included in the payload.

When a create, update or delete happens on a record of that entity, Autotask sends a callout: an HTTP `POST` with a JSON body to your webhook URL on port 443. When the webhook itself is deactivated or deleted, Autotask sends a callout to a second URL you provide, the deactivation URL.

Each callout is signed with a secret key you choose. The `X-Hook-Signature` header holds `sha1=` followed by the base64 HMAC-SHA1 of the request body.

## Autotask webhook features

| Feature | Details |
| --- | --- |
| Configuration | REST API (v1.0 and later) or SOAP API (v1.6 and later) only |
| Permissions | An API-only resource whose security level has **Can create WebHooks** enabled |
| Entities | Companies, Contacts, Assets (Configuration Items), Tickets, Ticket Notes |
| Required URLs | `WebhookUrl` for callouts and `DeactivationUrl` for deactivation notices |
| Triggers | Create and delete fire for every record. Updates fire when a subscribed field or UDF changes |
| Signature header | `X-Hook-Signature` |
| Scheme | HMAC-SHA1, base64, prefixed with `sha1=` |
| Signed content | The request body only |
| Secret | A `SecretKey` you choose, up to 64 characters |
| Latency | Usually under a minute, up to five minutes under heavy load |
| Throughput | Above 1,500 callouts per rolling hour per entity, further callouts queue |
| Limit | 50 active webhooks per entity per site |

Autotask doesn't publish an example callout body, its retry schedule, a response timeout or source IPs.

## Callouts and the payload

There is no event name. The `Action` field tells you what happened, and `EntityType` tells you to which kind of record.

| `Action` | Sent when | `Fields` contains |
| --- | --- | --- |
| `Create` | A record is created | All subscribed fields and UDFs that are non-null |
| `Update` | A subscribed field or UDF changes | The subscribed fields and UDFs that changed |
| `Delete` | A record is deleted | Nothing beyond the base fields |
| `Deactivated` | The webhook is deactivated | Nothing beyond the base fields. `Guid` identifies the webhook |

Every callout carries the same base fields:

* `Action`: `Create`, `Update`, `Delete` or `Deactivated`.
* `Guid`: a globally unique identifier for the callout.
* `EntityType`: the Autotask entity the callout is about.
* `Id`: the ID of the record in Autotask.
* `Fields`: the fields described above.
* `EventTime`: when the webhook fired.
* `SequenceNumber`: a number that increases with each callout. Gaps mean items were sent to the error log.
* `PersonID`: the Autotask resource ID of the user whose action in the UI triggered the callout.

Autotask doesn't print the exact values of `EntityType`. Since each webhook belongs to one entity, a receiver with one route or one Hookdeck source per webhook already knows which entity it is handling.

## Setting up Autotask webhooks

### Allow webhook creation

Webhooks are created by your integration's API-only resource, and its security level must have **Can create WebHooks** enabled. An administrator sets this on the security level in Autotask's admin settings.

### Create the webhook

Choose the secret key first. Autotask allows up to 64 letters, numbers and symbols and recommends at least 10 characters, so generate a long random value and store it as `AUTOTASK_WEBHOOK_SECRET`. Then create the webhook on your API zone:

```bash
curl -X POST "https://webservices[n].autotask.net/atservicesrest/v1.0/CompanyWebhooks" \
  -H "Username: $AUTOTASK_API_USER" \
  -H "Secret: $AUTOTASK_API_SECRET" \
  -H "APIIntegrationcode: $AUTOTASK_INTEGRATION_CODE" \
  -H "Content-Type: application/json" \
  -d '{
    "IsActive": true,
    "Name": "Company sync",
    "WebhookUrl": "https://example.com/webhooks/autotask",
    "DeactivationUrl": "https://example.com/webhooks/autotask/deactivated",
    "IsSubscribedToUpdateEvents": true,
    "SecretKey": "'"$AUTOTASK_WEBHOOK_SECRET"'",
    "SendThresholdExceededNotification": true,
    "NotificationEmailAddress": "ops@example.com"
  }'

```

The response is the new webhook's `itemId`. Both `WebhookUrl` and `DeactivationUrl` are required.

### Add trigger fields

Look up field IDs with `GET .../CompanyWebhookFields/entityInformation/fields`, then add each one to the webhook:

```bash
curl -X POST "https://webservices[n].autotask.net/atservicesrest/v1.0/CompanyWebhooks/4/Fields" \
  -H "Username: $AUTOTASK_API_USER" \
  -H "Secret: $AUTOTASK_API_SECRET" \
  -H "APIIntegrationcode: $AUTOTASK_INTEGRATION_CODE" \
  -H "Content-Type: application/json" \
  -d '{ "FieldID": 7, "IsSubscribedField": true, "IsDisplayAlwaysField": true, "WebhookID": 4 }'

```

`IsSubscribedField` makes a change to the field fire the webhook, and `IsDisplayAlwaysField` includes the field in every payload. Create and update callouts need at least one field. User-defined fields work the same way through `<EntityName>WebhookUdfFields`.

If your integration writes to Autotask, add its own API resource to `<EntityName>WebhookExcludedResources`, so its changes don't trigger callouts and loop back into your system.

If you use Hookdeck, create a source with the Autotask source type, paste the same `SecretKey` into it, and use the Hookdeck source URL as `WebhookUrl`.

## Securing Autotask webhooks

Read `X-Hook-Signature`, check that it starts with `sha1=`, and take the rest. Compute the HMAC-SHA1 of the raw request body with the `SecretKey`, encode it as base64 (keeping the `=` padding), and compare the two in constant time. Autotask doesn't provide an SDK for this.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyAutotaskSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret || !signatureHeader.startsWith('sha1=')) return false;
  const received = Buffer.from(signatureHeader.slice('sha1='.length), 'utf8');
  const expected = Buffer.from(
    crypto.createHmac('sha1', secret).update(rawBody).digest('base64'),
    'utf8'
  );
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/autotask', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyAutotaskSignature(req.body, req.get('x-hook-signature'), process.env.AUTOTASK_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const callout = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on callout.Guid, then route on callout.Action and callout.EntityType.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import hashlib
import hmac

def verify_autotask_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not secret or not signature_header.startswith("sha1="):
        return False
    received = signature_header[len("sha1="):]
    expected = base64.b64encode(hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha1).digest())
    return hmac.compare_digest(received.encode("utf-8"), expected)

# In FastAPI: raw_body = await request.body()
# verify_autotask_signature(raw_body, request.headers.get("x-hook-signature"),
#                           os.environ["AUTOTASK_WEBHOOK_SECRET"])

```

Note that the hash is SHA-1, not SHA-256, and the digest is base64, not hex. Autotask suggests checking your understanding with an online HMAC generator set to SHA-1 with base64 output. There is no timestamp in the signature, so there is no replay window to check.

> **Make Autotask webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `X-Hook-Signature` header on every callout, stores each company, contact and ticket change, and delivers it to your integration at a rate it can handle.

## Autotask webhook limitations and pain points

### No published retry schedule, and deactivation at the end

**The Problem:** An outage on your side ends with the webhook switched off.

**Why It Happens:** Autotask retries a failed callout behind the scenes, but doesn't say how often or for how long. If the callout keeps failing, the webhook is deactivated and Autotask calls your deactivation URL.

**Workarounds:**

* Handle the deactivation URL: alert on it, because it means callouts have stopped.
* After an outage, reactivate the webhook through the API and reconcile changed records by querying for recent updates.

**How Hookdeck Can Help:** Hookdeck accepts Autotask's callouts as soon as they arrive, so failures on your side don't count against the webhook. Callouts wait in Hookdeck and are delivered with [retries](/docs/retries) you configure.

### SHA-1 and base64 trip up copied code

**The Problem:** Verification fails on every callout even with the right key.

**Why It Happens:** Most webhook samples use SHA-256 and hex. Autotask uses HMAC-SHA1 with a base64 digest and a `sha1=` prefix, and stripping the base64 `=` padding also breaks the comparison.

**Workarounds:**

* Use `sha1` and `base64` exactly as shown above.
* Compare the base64 strings as they are, padding included.

**How Hookdeck Can Help:** Hookdeck's Autotask source type applies the right algorithm and encoding, so your service only needs to trust Hookdeck.

### Throughput limits during bulk changes

**The Problem:** After an import or bulk update, callouts arrive many minutes late.

**Why It Happens:** Above 1,500 callouts per rolling hour for an entity, Autotask queues further callouts until the rate drops below the threshold.

**Workarounds:**

* Set `SendThresholdExceededNotification` to `true` and a `NotificationEmailAddress`, so you know when the threshold is crossed.
* Subscribe only to the fields you need, so routine edits don't use up the hourly allowance.

**How Hookdeck Can Help:** Hookdeck records when each callout arrived, which makes a threshold delay easy to see, and it can deliver bursts to your service at a steady rate.

### Callout loops

**The Problem:** Your integration updates a ticket, which fires a callout, which makes your integration update the ticket again.

**Why It Happens:** Changes made through the API trigger webhooks like any other change.

**Workarounds:**

* Add your integration's API resource to `<EntityName>WebhookExcludedResources`.
* Ignore callouts whose `PersonID` is your integration's resource.

**How Hookdeck Can Help:** A Hookdeck [filter](/docs/filters) on `PersonID` can drop callouts caused by your own integration before they reach it.

## Best practices

### Verify against the raw body

The HMAC covers the body exactly as sent. Use `express.raw({ type: '*/*' })` in Express, `await request.text()` in Next.js and `await request.body()` in FastAPI, and parse only after the signature check passes.

### Deduplicate on `Guid`

`Guid` identifies a callout uniquely across Autotask databases. Record it before processing and skip repeats. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Use `SequenceNumber` for ordering

`SequenceNumber` always increases. Store the last number you processed per record and ignore older callouts. Gaps are expected, because some callouts go to Autotask's error log.

### Include the fields you route on

Set `IsDisplayAlwaysField` on the fields your handler needs every time, such as external IDs, so you don't have to call the API for each callout.

### Acknowledge fast and process asynchronously

Verify, store the callout, return `200`, and do the sync work afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Autotask webhooks are created per entity through the API and signed with `X-Hook-Signature`, a `sha1=`-prefixed base64 HMAC-SHA1 of the body keyed with the `SecretKey` you chose. Verify with SHA-1 and base64, deduplicate on `Guid`, order on `SequenceNumber`, exclude your own API resource to avoid loops, and watch the deactivation URL.

[Hookdeck Event Gateway](https://hookdeck.com) verifies every Autotask callout and holds it until your integration has processed it, so an outage on your side doesn't end with a deactivated webhook.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Autotask webhooks reliably in minutes.