50% off your first bill on Starter & Growth

Accounting Webhooks Integration Guide

Build a secure, reliable accounting API integration with NewLedger event types, signature verification, retries, timeouts, and recovery.

NT
NewLedger Team
EditorialAugust 22, 202610 min readUpdated August 23, 2026
A secure accounting event stream connecting a ledger to several business workflows

NewLedger Editorial

NewLedger webhooks notify your integration when committed accounting data changes. Use them to start a workflow; use the company-scoped API when you need the authoritative resource.

Quick reference

ContractValue
AcknowledgeAny 2xx response
SignatureHMAC-SHA256
Timestamp toleranceFive minutes
Dedupe keyEvent id
Delivery timeout15 seconds
Retry schedule1m → 10m → 1h → 8h → 24h → 48h

On this page

The delivery contract is asynchronous and at least once:

  • events can be delivered more than once
  • related events can arrive out of order
  • unavailable endpoints are retried
  • any 2xx response acknowledges the delivery
  • consumers must deduplicate before applying business effects

Event envelope

NewLedger sends a thin notification rather than a full resource snapshot:

{
  "id": "evt_0191a2b3-c4d5-7000-8000-000000000001",
  "type": "sales.invoice.updated",
  "version": "1.0",
  "company_id": "0191a2b3-c4d5-7000-8000-000000000010",
  "actor_id": "0191a2b3-c4d5-7000-8000-000000000030",
  "occurred_at": "2026-08-18T08:15:00.123Z",
  "resource": {
    "id": "0191a2b3-c4d5-7000-8000-000000000020",
    "type": "invoice"
  }
}
FieldPurpose
idImmutable event identity; use this for deduplication
typeStable accounting event name
versionEnvelope contract version
company_idCompany that owns the resource
actor_idInitiating actor when known
occurred_atTime the accounting change committed
resourcePublic resource type and ID to retrieve through the API

The notification is immutable. The referenced resource can change again before your integration retrieves it.

Receiver flow

Your endpoint receives and verifies the signed notification, persists its event ID for deduplication, and acknowledges it. Processing then follows one of two paths:

  • Hydratable resource: retrieve the latest state when the NewLedger API documentation provides a corresponding read operation.
  • Notification-only event: use the verified event as a signal. Payment and credit-note application events identify a document_payment, not its parent document, and cannot be hydrated by ID in v1. Also subscribe to the corresponding *.status.changed event when you need document state, or reconcile through the relevant list API.
Integration flowFrom webhook notification to current accounting data
NewLedgerSend notification

Event ID + resource reference

1Your endpointReceive + verify

Raw body, signature, timestamp

2Event-aware routingChoose data path

Hydrate supported resources or use the notification

3Your systemRun your workflow

Process asynchronously and idempotently

Acknowledge: return 2xx after the signed notification has been accepted. How your system schedules and executes the resulting work is up to you.

Return 2xx after durably accepting a valid notification. Hydration and business processing should run asynchronously. Use the immutable event ID to ensure a repeated delivery does not repeat the same business effect.

Event catalog

Event names follow <domain>.<resource>[.<subresource>].<action>. Subscriptions contain explicit event names, so future catalog additions do not silently expand an existing endpoint's access.

GroupEvents
Accounting · Accountaccounting.account.created
accounting.account.updated
accounting.account.archived
accounting.account.restored
accounting.account.deleted
Accounting · Journalaccounting.journal.created
accounting.journal.updated
accounting.journal.posted
accounting.journal.voided
accounting.journal.deleted
Contacts · Clientcontacts.client.created
contacts.client.updated
contacts.client.deleted
Contacts · Vendorcontacts.vendor.created
contacts.vendor.updated
contacts.vendor.deleted
Sales · Invoicesales.invoice.created
sales.invoice.updated
sales.invoice.status.changed
sales.invoice.payment.recorded
sales.invoice.payment.reversed
Purchases · Billpurchases.bill.created
purchases.bill.updated
purchases.bill.status.changed
purchases.bill.payment.recorded
purchases.bill.payment.reversed
Sales · Credit notesales.credit_note.created
sales.credit_note.updated
sales.credit_note.status.changed
sales.credit_note.applied
sales.credit_note.application_reversed
Expenses · Expenseexpenses.expense.created
expenses.expense.updated
expenses.expense.deleted
expenses.expense.status.changed
Items · Itemitems.item.created
items.item.updated
items.item.deleted
Company · Profilecompany.profile.updated
company.profile.deleted
company.status.changed
company.logo.updated
company.logo.deleted
Company · Usercompany.user.created
company.user.updated
company.user.status.changed
company.user.role.changed
company.user.deleted
Settings · Currencysettings.currency.created
settings.currency.deleted
Settings · Payment termsettings.payment_term.created
settings.payment_term.updated
settings.payment_term.deleted
Settings · Payment modesettings.payment_mode.created
settings.payment_mode.updated
settings.payment_mode.deleted
Settings · Email templatesettings.email_template.updated
Settings · SMTPsettings.smtp.updated
settings.smtp.deleted
Settings · Payment gatewaysettings.payment_gateway.created
settings.payment_gateway.updated
settings.payment_gateway.deleted

An invoice change and its accounting journal posting are separate commits. Subscribe to the business transitions you use rather than infer one event from another.

Endpoint verification event

The 62 names above are subscribable business events. Endpoint activation also sends a required, non-catalog event with type: "webhook.endpoint.verification" and resource.type: "webhook_endpoint".

Verify it with the same raw-body HMAC headers and return 2xx. Do not reject it with a business-event allowlist, and do not run a business workflow for it. Otherwise the endpoint remains in pending_verification.

Signing and verification

Each endpoint has its own signing secret, separate from App Connect authentication. Review API credentials versus OAuth 2.0 when choosing how the receiver will authenticate its follow-up API request. NewLedger signs the timestamp plus the exact raw request body with HMAC-SHA256.

signed_payload = timestamp + "." + raw_body
signature      = HMAC-SHA256(endpoint_secret, signed_payload)
HeaderValue
Webhook-Signaturet=<unix-seconds>;v1=<lowercase-hex>
Webhook-Event-IdImmutable event ID
Webhook-Delivery-AttemptAttempt number
User-AgentPartner identity, for example NewLedger-Webhooks/1.0

Verify requests in this order:

  1. Capture the body before JSON middleware transforms it.
  2. Parse the timestamp and one or more v1 signatures.
  3. Reject a timestamp outside the five-minute tolerance.
  4. Calculate HMAC-SHA256 over timestamp + "." + raw_body.
  5. Compare the expected and supplied values in constant time.
  6. Parse the event and store its ID only after verification succeeds.

Complete Node.js and TypeScript receiver

This Express example preserves the raw bytes, accepts either trusted secret during rotation, compares signatures in constant time, durably accepts the event, and returns before hydration or business processing. Configure express.raw before any JSON parser for this route.

import express, { type Request, type Response } from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
const trustedSecrets = [
  process.env.WEBHOOK_SECRET_CURRENT,
  process.env.WEBHOOK_SECRET_PREVIOUS,
].filter((secret): secret is string => Boolean(secret));

function verifyWebhook(rawBody: Buffer, header: string): boolean {
  const parts = header.split(";").map((part) => part.trim());
  const timestamp = parts.find((part) => part.startsWith("t="))?.slice(2);
  const supplied = parts
    .filter((part) => part.startsWith("v1="))
    .map((part) => part.slice(3));

  if (!timestamp || !/^\d+$/.test(timestamp) || supplied.length === 0) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) return false;

  const signedPayload = Buffer.concat([
    Buffer.from(`${timestamp}.`, "utf8"),
    rawBody,
  ]);

  return trustedSecrets.some((secret) => {
    const expected = createHmac("sha256", secret)
      .update(signedPayload)
      .digest();

    return supplied.some((hex) => {
      if (!/^[0-9a-f]{64}$/i.test(hex)) return false;
      const actual = Buffer.from(hex, "hex");
      return actual.length === expected.length && timingSafeEqual(actual, expected);
    });
  });
}

type WebhookEvent = { id: string; type: string };

async function persistForProcessing(event: WebhookEvent): Promise<void> {
  // Insert event.id with a unique constraint and enqueue the event atomically.
  // A worker—not this request—hydrates available resources and runs workflows.
  console.log(event);
}

app.post(
  "/webhooks/newledger",
  express.raw({ type: "application/json" }),
  async (req: Request, res: Response) => {
    const signature = req.get("Webhook-Signature");
    if (!signature || !verifyWebhook(req.body, signature)) {
      return res.status(401).send("Invalid signature");
    }

    const event = JSON.parse(req.body.toString("utf8")) as WebhookEvent;

    if (event.type === "webhook.endpoint.verification") {
      return res.sendStatus(204);
    }

    // Keep this durable handoff short; never hydrate or run business logic here.
    await persistForProcessing(event);
    return res.sendStatus(204);
  },
);

Rotate a signing secret without interrupting delivery

NewLedger keeps the previous secret usable for 24 hours through dual-signing. It does not choose one secret or the other: during the overlap, every production delivery is signed by both.

Webhook-Signature: t=<unix-seconds>;v1=<current-signature>;v1=<previous-signature>

Both signatures use the same timestamp and exact raw request body. The receiver calculates the expected signature with each secret it currently trusts and accepts the request when either supplied v1 value matches.

Rotation behaviorWhat it means
New secret becomes currentUse it for all new receiver deployments immediately
Previous secret remains valid for 24 hoursExisting deployments can continue verifying deliveries during rollout
At most two secrets are activeRotating again replaces the previous secret and starts a new 24-hour window
Secret reveal returns the current secret onlyKeep the previous value in your secret manager until the overlap ends
Previous secret expires automaticallyAfter 24 hours, deliveries contain only the current signature
Endpoint verification uses the current secretTest new endpoint configuration with the newly rotated value

Recommended rollout:

  1. Rotate the endpoint secret and save the new value immediately.
  2. Add the new secret to the receiver while retaining the previous secret.
  3. Deploy verification that checks both trusted secrets.
  4. Confirm live requests verify with the current signature.
  5. Remove the previous secret after the 24-hour overlap.

Delivery retries

NewLedger makes one immediate attempt followed by up to six automatic retries. Each delay is measured from the preceding failed attempt and receives independent jitter.

Immediate → 1 min → 10 min → 1 hr → 8 hr → 24 hr → 48 hr
ResultDelivery behavior
Network failureRetry
408, 425, 429Retry
5xxRetry
Other 4xxTerminal failure
Any 2xxAcknowledged

A valid Retry-After response on 429 is honored within the remaining retry and retention window. Every retry retains the original event ID and stored payload while incrementing Webhook-Delivery-Attempt.

Timeouts and redirects

RequestConnection timeoutTotal timeoutRedirects
Production delivery5 seconds15 secondsDisabled
Endpoint verification3 seconds5 secondsDisabled

Manage endpoints under Settings → Webhooks. Endpoint verification uses the same signed request path and HMAC headers as production delivery, but sends the distinct webhook.endpoint.verification event. Creating an endpoint places it in pending_verification; a 2xx response activates it. Changing the URL requires verification again.

Idempotent processing

Event deduplication protects the receiver. Each downstream action should also have an idempotency key so a repeated delivery cannot repeat the outcome.

evt_123:activate_subscription
evt_123:notify_account_owner
evt_123:refresh_revenue_projection

Use a database uniqueness constraint or pass the key to a downstream API that supports idempotency. Do not rely on an in-memory cache.

WorkflowOut-of-order strategy
Dashboard, CRM mirror, current statusFetch the latest resource from the API
Payment or application eventTreat as notification-only; pair with *.status.changed or reconcile through list APIs
Transition-specific automationHandle each event type separately and idempotently; do not infer ordering from a resource version

Endpoint health and recovery

Delivery history keeps the immutable request payload and attempt diagnostics. A manual resend creates a new delivery record; it does not rewrite history.

After three separate events exhaust their retry schedules without an intervening success, the endpoint pauses. A successful event resets that consecutive-exhaustion count.

Resuming enables new subscribed events. It does not backfill the paused period. Recover by reconciling against company-scoped resource APIs or ledger exports, then resume live delivery.

Production checklist

  • Verify the raw body before parsing JSON
  • Enforce the five-minute timestamp tolerance
  • Store endpoint secrets outside source control
  • Add a unique constraint for the event ID
  • Return 2xx after accepting a valid notification
  • Accept webhook.endpoint.verification without running a business workflow
  • Move hydration and business processing outside the request path
  • Make each business effect idempotent
  • Tolerate out-of-order delivery
  • Monitor failed processing, exhausted delivery retries, and paused endpoints
  • Test duplicate, delayed, reordered, and replayed events
  • Document the reconciliation procedure

For embedded deployments, the same webhook contract can sit behind a white-label accounting platform without exposing implementation-specific headers to customers.

# accounting-webhooks # webhook-integration # webhook-signature-verification # accounting-api # api-integration # event-driven-accounting # automation # newledger

Build your accounting integration

Create a NewLedger workspace, then open Settings → Webhooks to configure and test signed delivery.