Gareth Wilson Gareth Wilson

Guide to Sovos Webhooks: Features and Best Practices

Published


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. 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

FeatureDetails
Delivery platformSvix
Event configurationSovos side only: Compliance Network UI per organisation, or the Indirect Tax API for all organisations
Communication methodpushingV2
Endpoint managementSvix Consumer App Portal
Event fieldBody eventType, for example document.received
SignatureSvix scheme, verified with the Svix library or manually
Response window30 seconds, 2xx
RetriesExponential backoff (Sovos gives no schedule)
Portal optionsThrottling, 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:

{
  "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.

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:

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 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.

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.

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 verifies Sovos's signatures, responds on time, and holds each notification until your ERP integration has processed it.

Get started with Hookdeck for free and handle Sovos webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.