Acumatica · Integration

Acumatica Twilio SMS Integration — A Complete Guide

Acumatica Twilio SMS Integration — A Complete Guide is the kind of integration that pays for itself the first time it runs without intervention.

John Kihiu12 min read

Email is where notifications go to be ignored. In the markets I work in — Kenya, Uganda, Tanzania — SMS is still the channel that gets read within minutes, which is why "text the customer when their order ships" and "text the salesperson when their quote is approved" are recurring requests on Acumatica projects here. Twilio is the API I reach for when the client operates across countries; below is the architecture I deploy, plus the regional gotchas that no US-centric tutorial will warn you about.

The shape: business event → relay → Twilio

Acumatica has no native SMS provider, so every SMS integration is the same three-piece pattern:

  1. A Generic Inquiry exposing the trigger rows and every field the message needs (order number, customer phone, amount).
  2. A business event on that GI (trigger by record change — e.g. shipment status flips to Confirmed) with a webhook subscriber.
  3. A small relay — I use an Azure Function or a Laravel endpoint depending on what the client already hosts — that receives the event, normalises the phone number, and calls Twilio's Messages API.

Could you call Twilio directly from a graph extension on RowPersisted? You could, and I've ripped that code out of two implementations. An HTTP call inside the persist cycle means a slow or failing Twilio request blocks the user saving a shipment, and a transaction rollback after the call means you texted a customer about a shipment that doesn't exist. Fire the event after commit and let the relay own the network I/O.

C# · RELAY: EVENT TO TWILIO
var evt = await JsonSerializer.DeserializeAsync<AcuEvent>(req.Body);
var row = evt.Inserted[0];

var to = PhoneUtil.ToE164(row["Phone1"], defaultRegion: "KE"); // +2547...
if (to == null)
    return Log.SkipInvalidNumber(row);   // 200 OK — don't make Acumatica retry bad data

var form = new Dictionary<string, string> {
    ["To"]   = to,
    ["From"] = _senderId,                // messaging service SID in practice
    ["Body"] = "Habari! Order " + row["OrderNbr"] +
               " has shipped via " + row["Carrier"] +
               ". Track: " + row["TrackingUrl"],
    ["StatusCallback"] = _baseUrl + "/twilio/status?ref=" + row["OrderNbr"]
};

var msg = new HttpRequestMessage(HttpMethod.Post,
    "https://api.twilio.com/2010-04-01/Accounts/" + _accountSid + "/Messages.json");
msg.Headers.Authorization = new AuthenticationHeaderValue("Basic",
    Convert.ToBase64String(Encoding.ASCII.GetBytes(_accountSid + ":" + _authToken)));
msg.Content = new FormUrlEncodedContent(form);

var resp = await _http.SendAsync(msg);
resp.EnsureSuccessStatusCode();

Phone numbers: the actual hard part

The Twilio call is trivial. The data quality in Acumatica's phone fields is not. In every live database I've touched, Phone1 contains a mixture of 0722 123 456, +254722123456, 0722-123-456, and the occasional "call John first". Twilio wants strict E.164. Your relay must normalise with a proper library (libphonenumber ports exist for .NET and PHP) and a sensible default region, and it must skip rather than fail on garbage — returning a non-2xx to Acumatica for a bad phone number just makes the business event retry a message that can never succeed.

Longer term, the better fix is upstream: a validation attribute on the DAC that normalises numbers at entry. I usually ship the relay-side normaliser first (works today) and the entry-side validator second (fixes the data over months).

Sender IDs, and why Kenya is special

Here's the part the tutorials miss. In Kenya — and most East African markets — you cannot just buy a Twilio long-code number and start blasting transactional SMS to local subscribers:

Closing the loop with status callbacks

That StatusCallback URL in the code above is not optional in my deployments. Twilio POSTs you sent, delivered, undelivered, failed transitions per message. The callback handler writes the final status back into Acumatica via the contract-based REST API — I usually keep a simple custom table (notification log) bound to a GI so support staff can answer "did the customer get the text?" without a Twilio login. Two rules: validate Twilio's X-Twilio-Signature header on the callback endpoint, and make the write idempotent on MessageSid because callbacks can arrive out of order and more than once.

Never put the auth token in a customization project file

Twilio credentials belong in the relay's secret store (Key Vault, environment config), not in Acumatica automation step parameters or hard-coded in a published customization that gets copied between tenants. I've seen a production auth token in a test tenant's webhook URL. Rotate keys the day you inherit a project.

Bursts, quiet hours, and cost control

Business events will happily fire 500 webhooks when someone mass-confirms shipments after a stock take. Three defences I ship as standard: the relay enqueues instead of sending inline (a queue also smooths Twilio's own rate limits on long codes); a quiet-hours check (nobody wants a 2 a.m. dunning SMS, and in some jurisdictions marketing texts at night are a compliance issue); and a monthly cap with an alert, because SMS is the one notification channel where a bug has a direct per-unit price. A loop that emails you 10,000 times is embarrassing. A loop that texts 10,000 times is an invoice.

Wrapping up

The Twilio API is the easy afternoon; the project is everything around it. Fire from business events rather than inside the persist cycle, normalise phone numbers defensively and skip the unfixable ones, register your sender ID before you write a line of code if you're sending into East Africa, and wire status callbacks back into an Acumatica-visible log so delivery is a fact, not a hope. Do those four things and SMS becomes the most reliably read channel your ERP has.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.