Agent skill

Acumatica Webhooks Skill

Receive and authenticate Acumatica push notification webhooks. Use when setting up an Acumatica (MYOB Acumatica) Webhook destination on the Push Notifications form, checking the secret header you configured, debugging 401s, or handling Inserted and Deleted row changes from a monitored generic inquiry.

Install this skill

npx skills add hookdeck/webhook-skills --skill acumatica-webhooks


Acumatica ERP sends a push notification to a Webhook destination (Push Notifications form, SM302000) when the results of a monitored generic inquiry change. The request is an HTTP POST with a JSON body holding Inserted and Deleted row arrays plus Query, CompanyId, Id, TimeStamp and AdditionalInfo. Requests are not signed. The only built-in protection is the optional header you add in the destination's Header Name and Header Value boxes, and your endpoint authenticates the request by comparing that header with the value you stored.

When to Use This Skill

  • How do I receive Acumatica push notifications as webhooks?
  • How do I authenticate an Acumatica Webhook destination? (There is no signature: use a secret header)
  • Which header does Acumatica send, and why is the name configurable?
  • How do I tell inserted, updated and deleted rows apart in an Acumatica payload?
  • Why does my Acumatica webhook endpoint return 401?

Verification (core)

const crypto = require('crypto');

// Equal-length Buffers only: timingSafeEqual throws on unequal lengths.
function safeEqual(a, b) {
  const x = Buffer.from(a, 'utf8');
  const y = Buffer.from(b, 'utf8');
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

function verifyAcumaticaRequest(headers, env = process.env) {
  const secret = env.ACUMATICA_WEBHOOK_SECRET;
  if (!secret) return false;                                  // never accept when unconfigured
  const name = (env.ACUMATICA_WEBHOOK_HEADER || 'Authorization').toLowerCase();
  const received = headers[name];                             // Node lowercases header names
  return typeof received === 'string' && safeEqual(received, secret);  // whole value, no parsing
}
  • The header name is whatever you typed in Header Name (default here: Authorization). The expected value is the exact whole string from Header Value, including a Bearer prefix if you typed one. Do not strip or parse the prefix.
  • Look the header up case-insensitively. Return 401 when it is missing or does not match, 400 for a body that isn't a JSON object, 200 once handled.
  • Read the body raw (express.raw({ type: '*/*' })) and authenticate before parsing it.
  • Don't set Accept or Content-Type as the custom header; Acumatica sets those itself.

A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Serve the endpoint over HTTPS only, and rotate the header value in Acumatica and in ACUMATICA_WEBHOOK_SECRET if it ever leaks.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Notification Shape

There is no event-type field. The Query value (the generic inquiry name, or the class name of a built-in definition) says what the notification is about. What happened to a row comes from which arrays it appears in.

ChangeAppears in
Row insertedInserted only
Row deletedDeleted only
Row updatedDeleted (old values) and Inserted (new values)
FieldMeaning
Inserted, DeletedRow objects, one key per inquiry column
QueryName of the generic inquiry or built-in definition
CompanyIdTenant name
IdUnique id of the data transaction; use it to drop duplicate notifications
TimeStampLong integer for ordering notifications
AdditionalInfoExtra system or custom information (often {})

Acumatica makes at most five attempts, and resends when your endpoint replies with an error, so reply 200 once the notification is received and dedupe on Id.

Environment Variables

ACUMATICA_WEBHOOK_HEADER=Authorization        # the Header Name you set on the destination
ACUMATICA_WEBHOOK_SECRET=your-header-value    # the exact Header Value, e.g. "Bearer abc123"

Local Development

Acumatica can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 acumatica --path /webhooks/acumatica

Use the printed URL as the Address of the Webhook destination.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 6, 2026

View on GitHub →