# 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)

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/acumatica-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/acumatica-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/acumatica-webhooks/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.

| Change | Appears in |
| --- | --- |
| Row inserted | `Inserted` only |
| Row deleted | `Deleted` only |
| Row updated | `Deleted` (old values) and `Inserted` (new values) |

| Field | Meaning |
| --- | --- |
| `Inserted`, `Deleted` | Row objects, one key per inquiry column |
| `Query` | Name of the generic inquiry or built-in definition |
| `CompanyId` | Tenant name |
| `Id` | Unique id of the data transaction; use it to drop duplicate notifications |
| `TimeStamp` | Long integer for ordering notifications |
| `AdditionalInfo` | Extra 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

```bash
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):

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

```

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

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/acumatica-webhooks/references/overview.md) - What push notifications are, payload shape and change types
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/acumatica-webhooks/references/setup.md) - Create the Webhook destination and secret header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/acumatica-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/acumatica-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests