# Guide to Sovos Webhooks: Features and Best Practices

Sovos webhooks push e-invoicing notifications from Sovos's Indirect Tax API to your ERP system. When a document you sent or received moves through the Compliance Network, for example when a workflow completes or an error occurs, Sovos sends a notification to your endpoint instead of waiting for you to poll for it.

This guide covers how Sovos webhooks are configured, what a notification looks like, how to verify one, and how to run a Sovos receiver in production. It applies to the Indirect Tax API's Svix-based push method. Other Sovos products are not covered.

## What are Sovos webhooks?

Sovos delivers Indirect Tax API notifications through [Svix](/webhooks/platforms/guide-to-svix-webhooks-features-and-best-practices). Sovos decides which events are sent, and you manage endpoints, delivery logs and retries in the Svix Consumer App Portal.

Events are configured by a Sovos partner or by Sovos Professional Services, and Sovos says you can't add or filter events in the Consumer App Portal. The portal is where you add your URL and tune delivery.

## Sovos webhook features

| Feature | Details |
| --- | --- |
| Delivery platform | Svix |
| Event configuration | Sovos side only: Compliance Network UI per organisation, or the Indirect Tax API for all organisations |
| Communication method | `pushingV2` |
| Endpoint management | Svix Consumer App Portal |
| Event field | Body `eventType`, for example `document.received` |
| Signature | Svix scheme, verified with the Svix library or manually |
| Response window | 30 seconds, `2xx` |
| Retries | Exponential backoff (Sovos gives no schedule) |
| Portal options | Throttling, custom headers, transformations, OAuth and mTLS |

## Common events

Sovos configures events by name in a `statusEvent` field. Its configuration example uses `workflow.completed` and `system.error`, and `document.completed` also appears in its docs. Sovos points to its samples for the full list.

Every notification uses the same body. This is Sovos's example:

```json
{
  "appPrefix": "DLT",
  "contentUrl": "https://uat-einvoicing-api-emea.sovos.com/download/api/v1/download/notification/P4pob2kFJrwkgZ04...",
  "correlationId": "822431a1-ef99-4ef7-8dfa-38f9c6b6b0af-mock",
  "createdDate": 1770290583638,
  "eventType": "document.received",
  "metadata": {
    "documentId": "4e9528cdc7cbe2e5444a70b9a4314bcb",
    "erpDocumentId": "#BC-UAT-MOCK582230",
    "erpSystemId": "TestSystemId",
    "processType": "0",
    "productId": "WA_SPARK__2.0",
    "sciCloudStatusCode": "100",
    "sciGovtStatusCode": null,
    "sciResponseCode": "IP",
    "sciStatusAction": "NOA",
    "taxId": "WA_SPARK_TaxId",
    "transactionId": "7d7f0b4f-f60f-4be3-931f-b5ba8ab26ca6"
  },
  "notificationId": "bd92d169-7ec0-4f21-a7eb-e2238070d9d7",
  "orgId": "faf412ed-bb3f-4e2c-9c7e-6bcf280e1ccf"
}

```

`contentUrl` is a download URL for the notification's content, `createdDate` is in epoch milliseconds, and `metadata.erpDocumentId` ties the notification back to the document in your ERP. Sovos doesn't document how a configured `statusEvent` maps to the `eventType` in the body, so log the values you receive before you route on them.

## Setting up Sovos webhooks

1. A partner or Sovos Professional Services configures your ERP system, then sets the communication method to `pushingV2` and adds events. In the Compliance Network UI this is done per organisation. Through the API it is `POST /v2/configurations/workspaces/settings`, which only partners and Professional Services can call.
2. Build an endpoint that accepts JSON `POST` requests over HTTPS, uses an authentication layer, responds within 30 seconds with a `2xx`, and handles duplicates.
3. Open the Consumer App Portal, from the Compliance Network UI or with the link returned by the Indirect Tax API.
4. Add your endpoint, set a throttling value (the most messages per second it accepts), and ignore the portal's event filtering.
5. Copy the endpoint's signing secret and store it server-side, for example as `SOVOS_WEBHOOK_SECRET`.
6. Send an example event from the portal and check the delivery log.

The communication method also has an `autoAcknowledgement` option, which Sovos recommends setting to `true` so that received notifications are marked as read automatically.

## Securing Sovos webhooks

Sovos requires you to verify each message with the Svix library or Svix's manual method. It doesn't restate the scheme. Svix signs `{id}.{timestamp}.{raw body}` with HMAC-SHA256, keyed with the base64-decoded part of a `whsec_` secret, and sends the result base64-encoded in a signature header as `v1,<signature>`. Sovos doesn't name the header prefix; the code below uses Svix's default `svix-` headers.

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

function verifySovosSignature(rawBody, headers, secret) {
  const msg_id = headers['svix-id'];
  const msg_timestamp = headers['svix-timestamp'];
  const signature_header = headers['svix-signature'];
  if (!msg_id || !msg_timestamp || !signature_header) return false;
  if (typeof secret !== 'string' || !secret.startsWith('whsec_')) return false;

  // The HMAC key is the base64-decoded part of the secret after "whsec_".
  const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
  const signed_content = Buffer.concat([Buffer.from(`${msg_id}.${msg_timestamp}.`, 'utf8'), rawBody]);
  const expected = Buffer.from(
    crypto.createHmac('sha256', key).update(signed_content).digest('base64'),
    'utf8'
  );

  return signature_header.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    if (version !== 'v1' || !signature) return false;
    const received = Buffer.from(signature, 'utf8');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

const app = express();

app.post('/webhooks/sovos', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.SOVOS_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifySovosSignature(req.body, req.headers, secret)) {
    return res.sendStatus(400);
  }

  const notification = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on notification.notificationId, queue it, then respond.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import hashlib
import hmac
import os

def verify_sovos_signature(raw_body: bytes, headers, secret: str) -> bool:
    msg_id = headers.get("svix-id")
    msg_timestamp = headers.get("svix-timestamp")
    signature_header = headers.get("svix-signature")
    if not (msg_id and msg_timestamp and signature_header):
        return False
    if not secret or not secret.startswith("whsec_"):
        return False

    key = base64.b64decode(secret[len("whsec_"):])
    signed_content = f"{msg_id}.{msg_timestamp}.".encode("utf-8") + raw_body
    expected = base64.b64encode(hmac.new(key, signed_content, hashlib.sha256).digest())

    for entry in signature_header.split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature.encode("utf-8"), expected):
            return True
    return False

# In FastAPI: raw_body = await request.body()
# verify_sovos_signature(raw_body, request.headers, os.environ["SOVOS_WEBHOOK_SECRET"])

```

The Svix libraries handle this for you and also check the timestamp. If your first deliveries arrive with `webhook-` headers instead, swap the names; the scheme is the same.

> **Make Sovos webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies Sovos's Svix signature, answers inside the 30-second window, and queues every e-invoicing notification until your ERP integration has processed it.

Hookdeck's Sovos source type verifies Svix signatures with the secret you enter and accepts either the `svix-` or the `webhook-` header names. Verification is optional on the source, so leave it off if you didn't set a secret. Hookdeck does not apply a timestamp window.

## Sovos webhook limitations and pain points

### You can't choose events yourself

**The Problem:** You need a new event, or fewer events, and the portal won't let you change them.

**Why It Happens:** All event configuration is on the Sovos side, and only partners or Sovos Professional Services can configure it through the API.

**Workarounds:**

* Agree the event list with your partner or Professional Services contact before go-live.
* Handle unknown `eventType` values by logging and acknowledging them, so a new event doesn't cause failed deliveries.

**How Hookdeck Can Help:** Hookdeck filters can drop or route notifications by `eventType`, so you can narrow what reaches each service without waiting for a configuration change at Sovos.

### Undocumented event names and retry schedule

**The Problem:** You can't build a complete handler or plan for outages from the docs alone.

**Why It Happens:** Sovos lists `statusEvent` names for configuration, while bodies carry an `eventType`, and the mapping between them isn't spelled out. Retries are described only as exponential backoff.

**Workarounds:**

* Record the `eventType` values from test deliveries and route on those.
* Use the portal's delivery logs and retry button after an outage.

**How Hookdeck Can Help:** Hookdeck stores every notification with its headers and body, so you can see exactly what Sovos sends and replay anything your service failed to process.

### Compliance documents on a 30-second clock

**The Problem:** Downloading the document from `contentUrl` inside the handler makes responses slow.

**Why It Happens:** Sovos expects a `2xx` within 30 seconds, and fetching attachments or updating the ERP can take longer.

**Workarounds:**

* Store the notification, respond, then download from `contentUrl` in a background job.
* Set a throttling value in the portal that your endpoint can sustain.

**How Hookdeck Can Help:** Hookdeck acknowledges Sovos straight away and delivers to your service at a rate you set, so slow ERP updates don't cause Svix retries.

## Best practices

### Verify against the raw body

Use `express.raw()` in Express, `Buffer.from(await request.arrayBuffer())` in a Next.js route handler, or `await request.body()` in FastAPI, and parse only after verification.

### Deduplicate on `notificationId`

Sovos lists idempotency as a requirement. Store `notificationId` (or the delivery's Svix message ID) and skip repeats. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Use `correlationId` and `erpDocumentId` for tracing

Match notifications to the documents you submitted with `metadata.erpDocumentId`, and log `correlationId` and `metadata.transactionId` with your own records so you can trace a document across systems.

### Acknowledge fast and process asynchronously

Return a `2xx` once the notification is stored. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Sovos uses Svix to push Indirect Tax API notifications, with events chosen on the Sovos side and endpoints managed in the Consumer App Portal. Verify each notification with your endpoint's `whsec_` secret, deduplicate on `notificationId`, respond within 30 seconds, and fetch document content after you have acknowledged.

[Hookdeck Event Gateway](https://hookdeck.com) verifies Sovos's signatures, responds on time, and holds each notification until your ERP integration has processed it.

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