Most of the Teams integrations I get asked to build boil down to one sentence: "when something happens in Acumatica, tell the team about it in a channel." A sales order over a credit limit, a big AR invoice released, a case escalated. The good news is that this is one of the cheapest integrations you can ship, because both sides already speak HTTP: Acumatica has business events that can fire a webhook, and Teams has incoming webhooks that accept a JSON card. No middleware is strictly required for the simple case.
In this post I'll walk through the pattern I actually deploy: business event → (optionally a tiny relay function) → Teams channel, plus the gotchas around card formatting, rate limits, and the deprecation of the old Office 365 connectors.
The outbound trigger: business events, not polling
Don't build a poller for this. Notifications are exactly what business events exist for. Create a Generic Inquiry that surfaces the records you care about (say, sales orders with OrderTotal above a threshold), then define a business event of type Trigger by Record Change on insert. Attach a subscriber of type Webhook (import/notification webhook subscribers have been available since 2021 R1) and point it at your target URL.
The event payload gives you the GI row fields, which is why the GI matters more than people think: whatever columns you put in the inquiry are the fields available to your Teams message. Include the order number, customer name, total, and the branch — future you will want them.
The Teams side: workflows, not the old connectors
This is the gotcha that has bitten two of my clients already: the classic "Incoming Webhook" Office 365 connector is deprecated — Microsoft has been retiring O365 connectors in favour of Workflows (Power Automate). If you find an old tutorial telling you to add the Incoming Webhook connector to a channel, that path is on its way out. The replacement is the "When a Teams webhook request is received" trigger in a Workflow, which still hands you a plain HTTPS URL — so the Acumatica side doesn't change at all.
The payload format did change, though. The workflow trigger expects an Adaptive Card envelope rather than the legacy MessageCard:
{
"type": "message",
"attachments": [{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{ "type": "TextBlock", "size": "Medium", "weight": "Bolder",
"text": "Sales order SO006721 needs credit review" },
{ "type": "FactSet", "facts": [
{ "title": "Customer", "value": "MAERSK KENYA LTD" },
{ "title": "Total", "value": "KES 1,842,300.00" },
{ "title": "Branch", "value": "NAIROBI" }
]}
],
"actions": [{
"type": "Action.OpenUrl", "title": "Open in Acumatica",
"url": "https://erp.example.com/Main?ScreenId=SO301000&OrderType=SO&OrderNbr=SO006721"
}]
}
}]
}
Why I usually put a tiny relay in between
Acumatica's webhook subscriber sends the event payload in Acumatica's own JSON shape — it can't render an Adaptive Card for you. You have two options. Option one: let the Power Automate workflow itself parse the raw Acumatica payload and compose the card. That's zero code and fine for one or two notifications, but the parsing lives in a flow that nobody version-controls.
Option two, which I prefer once there are more than a couple of event types: a small Azure Function (or a Laravel route, on stacks where the client already runs one) that accepts the Acumatica event, maps fields, builds the card, and posts it on. Fifty lines of C#:
[Function("AcuToTeams")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
var evt = await JsonSerializer.DeserializeAsync<AcuEvent>(req.Body);
var row = evt.Inserted[0]; // GI columns keyed by field name
var card = TeamsCard.Build(
title: "Sales order " + row["OrderNbr"] + " needs credit review",
facts: new Dictionary<string, string> {
["Customer"] = row["CustomerName"],
["Total"] = row["CuryOrderTotal"] + " " + row["CuryID"]
},
link: _erpBaseUrl + "/Main?ScreenId=SO301000&OrderNbr=" + row["OrderNbr"]);
var resp = await _http.PostAsJsonAsync(_teamsWebhookUrl, card);
resp.EnsureSuccessStatusCode(); // non-2xx -> Acumatica retries the event
return req.CreateResponse(HttpStatusCode.OK);
}
The relay also gives you one place to add a shared-secret check (Acumatica lets you attach custom headers to the webhook subscriber — use them; a Teams-bound URL sitting open on the internet is an invitation).
The Action.OpenUrl button that jumps straight to the order in Acumatica is what turns the notification from noise into a workflow. Build the screen URL from ScreenId plus key fields — it survives upgrades better than copying URLs from the browser.
Rate limits and burst behaviour
Teams webhooks are throttled — roughly four requests per second, with tighter limits over longer windows. That's plenty for human-scale notifications and catastrophically insufficient if someone releases a 3,000-line batch and your business event fires per row. Two defences: first, scope the GI so the event triggers on the document, not the line. Second, if bursts are legitimate (month-end invoice runs), have the relay batch rows into a single summary card instead of one card per record. A channel that gets 400 cards in a minute gets muted by everyone in it, and then the integration is dead anyway.
What about Teams → Acumatica?
Approvals from inside Teams come up in every scoping call. It's doable — an Adaptive Card with Action.Execute posting to a bot or workflow that calls Acumatica's REST API to approve the document — but it's an order of magnitude more work than the notify direction, because now you need identity: which Teams user clicked, and are they the assigned approver in Acumatica? I've shipped it once, mapping Teams user AAD object IDs to Acumatica employee records via a custom cross-reference table. It worked, but for most teams the honest recommendation is the deep link: one click into Acumatica's own approval screen, where the security model already exists.
Wrapping up
Teams notifications from Acumatica are a half-day job if you keep the shape simple: a well-designed GI, a business event with a webhook subscriber, a Workflows URL on the Teams side, and — once you outgrow two event types — a small relay function that owns card formatting and secrets. Trigger on documents rather than lines, batch when bursts are expected, and put a deep link on every card. Save the inbound approval bot for when someone genuinely needs it.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.