Acumatica does not ship a "send this to a URL" subscriber. The three built-in subscriber types — email notification, import scenario, mobile push — all keep the effect inside the Acumatica instance or inside Acumatica's own notification pipeline. A webhook, in the sense of "POST this payload to an external HTTP endpoint when something happens," has to be built as a custom subscriber.
Why there's no built-in webhook subscriber
The gap makes sense once you think about what a generic webhook subscriber would need to handle safely: an arbitrary external URL, an arbitrary auth scheme, retry on 5xx and rate limits, and a way to surface delivery failures to someone. Acumatica leaves all of that to the integrator, which is more work up front but avoids a half-generic webhook feature that's wrong for half the endpoints anyone would actually point it at.
The shape of a custom webhook subscriber
The subscriber itself should not make the HTTP call. As covered in the Business Events deep dive, the subscriber runs inline with the save — a webhook target being slow or down should never be able to slow down or fail someone's shipment confirmation. The reliable pattern is two pieces: the subscriber writes a row to a local outbound queue table, and a separate scheduled processing screen (or an Automation Notification on a timer) reads pending rows and does the actual delivery, with retry and backoff isolated to that second piece.
public class OutboundWebhookProcessor : PXGraph<OutboundWebhookProcessor>
{
public PXProcessing<OutboundEventQueue> Queue;
public static void Process(List<OutboundEventQueue> batch)
{
var graph = PXGraph.CreateInstance<OutboundWebhookProcessor>();
foreach (var row in batch)
{
try
{
var payload = graph.BuildPayload(row);
var response = graph.HttpClient.PostJson(row.TargetUrl, payload,
headers: new { X-Idempotency-Key = row.EventId });
if (!response.IsSuccessStatusCode)
throw new PXException($"Webhook target returned {response.StatusCode}");
row.Status = "Delivered";
}
catch (Exception ex)
{
row.Attempts++;
row.Status = row.Attempts >= 5 ? "Failed" : "Pending";
row.LastError = ex.Message;
}
}
}
}
Idempotency keys are not optional
Because delivery is retried, the receiving side will see the same event more than once whenever a retry follows a delivery that actually succeeded but timed out on the response. Every payload needs a stable idempotency key — the triggering record's reference number plus the event type is usually enough — and the receiving endpoint is responsible for de-duplicating on it. This is the same discipline any payment or order webhook consumer already expects; Acumatica isn't special here, it's just the sender instead of the receiver.
If the webhook target is reachable from the public internet (an Azure Function, a Lambda URL, a third-party SaaS endpoint), include an HMAC signature over the payload using a shared secret, and have the receiver verify it before trusting the body. An unsigned webhook endpoint is an unauthenticated write path into whatever system consumes it.
What goes in the condition vs. the payload
Keep the trigger condition narrow (field changed to a specific value, not "row updated") and keep the payload builder separate from the condition logic. Business logic about what counts as "confirmed" belongs in the condition; formatting decisions about how the receiver wants the JSON shaped belong in the queue processor. Mixing the two means every time the receiving system's contract changes, you're back in the Business Events screen editing a trigger condition that had nothing to do with the change.
Wrapping up
Acumatica webhooks are a two-piece pattern: a Business Event subscriber that writes to a queue table, and a processor that delivers with retry, idempotency keys, and signed payloads. Treat the delivery half like you would any outbound integration — because that's exactly what it is.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.