Acumatica · REST API

Acumatica Twilio SMS Integration — A Complete Guide

Acumatica Twilio SMS Integration — A Complete Guide — a complete, field-tested reference by John Kihiu, Acumatica developer in Nairobi.

John Kihiu12 min read

Acumatica Twilio SMS Integration — A Complete Guide is the kind of integration that pays for itself the first time it runs without intervention. Most Acumatica integrations start as weekend projects and end as 6-month engagements — not because they are hard, but because the failure modes are not obvious until you have seen them. This guide is the field-tested pattern for Twilio, with the failure modes called out and the production hygiene that keeps the integration quiet at 3 AM.

To build, to buy, or to wait

Before you write a line of code, decide whether the integration is worth building. The decision matrix:

QuestionIf yesIf no
Does the provider have a documented, supported API?BuildWait for the provider to ship one
Is there an ISV solution on the Acumatica Marketplace?Buy (and customise)Build
Is the data volume > 100k records per day?Build a queue-based integrationSynchronous REST will do
Is the integration a one-off for one tenant?Build quickly, documentBuild carefully, version, package
Buy before you build (when you can)

If an ISV solution exists, buy it. The cost of an ISV solution is 2-4 weeks of subscription; the cost of building the same thing is 3-6 months of engineering. The ISV also handles the upgrade story, which is the most expensive part of a custom integration.

The data flow

Most integrations follow a four-step pattern: pull from source, transform, push to destination, reconcile. Each step has its own failure modes. The right way to build it:

PSEUDOCODE · THE FOUR STEPS
// 1. PULL: fetch from source
var sourceRecords = await sourceClient.FetchSince(lastSyncTimestamp);
// 2. TRANSFORM: map to Acumatica shape
var acumaticaRecords = sourceRecords.Select(MapToAcumatica).ToList();
// 3. PUSH: write to Acumatica (with idempotency)
foreach (var batch in acumaticaRecords.Chunk(100))
    await acumaticaClient.BulkUpsert(batch, idempotencyKey: r => r.ExternalRef);
// 4. RECONCILE: verify counts, alert on mismatch
if (Math.Abs(sourceCount - acumaticaCount) > threshold)
    await alert.Send($"Sync mismatch: source={sourceCount}, acumatica={acumaticaCount}");

For the broader Acumatica REST patterns, see the REST API definitive guide and the bulk data loads guide.

Authentication

Twilio uses HTTP Basic authentication with an Account SID and Auth Token. The flow:

  1. Register an OAuth client with the provider (developer portal).
  2. Configure the client ID, client secret, and redirect URI in a secrets manager (not in code).
  3. Request a token at the start of every integration run.
  4. Refresh the token before it expires (typically 5 minutes early).
  5. Handle 401 by re-authenticating and retrying once.
C# · OAUTH CLIENT
public class AcumaticaTwilioSmsIntegrationClient
{
    private readonly HttpClient _http;
    private string _accessToken;
    private DateTime _tokenExpiry = DateTime.MinValue;

    public async Task<string> GetAccessToken()
    {
        if (DateTime.UtcNow < _tokenExpiry.AddMinutes(-5)) return _accessToken;
        var form = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string,string>("grant_type", "client_credentials"),
            new KeyValuePair<string,string>("client_id", _clientId),
            new KeyValuePair<string,string>("client_secret", _clientSecret),
        });
        var resp = await _http.PostAsync("/oauth/token", form);
        resp.EnsureSuccessStatusCode();
        var token = await resp.Content.ReadFromJsonAsync<TokenResponse>();
        _accessToken = token.access_token;
        _tokenExpiry = DateTime.UtcNow.AddSeconds(token.expires_in);
        return _accessToken;
    }
}

For the broader OAuth pattern in Acumatica, see the OAuth 2.0 setup guide.

Idempotency is not optional

Every PUT to Acumatica must carry an idempotency key. The key is the natural identifier of the record — a customer reference, an order ID, an external system GUID. Without the key, a retry after a network timeout creates a duplicate. With the key, the retry is a no-op.

C# · IDEMPOTENT PUSH
foreach (var record in acumaticaRecords)
{
    var payload = new { ExternalRef = { value = record.ExternalRef } };
    var maxRetries = 5;
    for (int attempt = 0; attempt < maxRetries; attempt++)
    {
        try
        {
            var resp = await _http.PutAsJsonAsync(
                $"/entity/Default/24.200.001/INItemSite", payload);
            resp.EnsureSuccessStatusCode();
            break;
        }
        catch (HttpRequestException) when (attempt < maxRetries - 1)
        {
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
        }
    }
}
Concurrency exceptions

When two processes try to update the same record, Acumatica returns a concurrency exception. Re-fetch the record (which gets the new ETag), apply your change, retry. The retry is part of the contract, not a bug.

For the full retry and concurrency pattern, see the rate limiting and retry guide and the error handling guide.

Rate limits and the licence cap

Twilio has rate limits. Acumatica has a licence cap on concurrent users. Both must be respected. The right way:

C# · THROTTLED PUSH WITH BACKPRESSURE
var semaphore = new SemaphoreSlim(20);
var tasks = acumaticaRecords.Select(async record =>
{
    await semaphore.WaitAsync();
    try { await PushRecord(record); }
    finally { semaphore.Release(); }
});
await Task.WhenAll(tasks);

For the full performance and licence guidance, see the licence concurrency guide and the performance tuning guide.

Webhooks for event-driven work

If the provider supports webhooks, use them. Polling is a tax; webhooks are free. The pattern:

  1. Register your HTTPS endpoint with the provider.
  2. Validate the signature on every incoming webhook.
  3. Respond 200 fast; process async.
  4. Make your handler idempotent — webhooks are at-least-once delivery.
C# · WEBHOOK HANDLER
[HttpPost("/webhooks/acumatica-twilio-sms-integration")]
public async Task<IActionResult> Handle(AcumaticaTwilioSmsIntegrationEvent evt)
{
    if (!VerifySignature(Request)) return Unauthorized();
    _ = Task.Run(async () =>
    {
        try { await Process(evt); }
        catch (Exception ex) { _logger.LogError(ex, "Webhook processing failed"); }
    });
    return Ok();
}

For the full webhook pattern, see the webhooks vs polling guide.

Reconciliation and the runbook

The integration will fail. The question is how quickly you notice. The minimum hygiene:

  1. Log every push with the source ID, the Acumatica ID, the timestamp, and the response code.
  2. Run a daily reconciliation that compares source count to Acumatica count, alert on mismatch.
  3. Maintain a dead-letter table for records that failed to push; a worker replays them after fix.
  4. Track per-tenant error rate; alert on a spike.
C# · RECONCILIATION
var sourceCount = await sourceClient.CountSince(lastSync);
var acumaticaCount = await acumaticaClient.CountWhere($"ExternalRef like 'SRC-%' AND CreatedAt > '{lastSync:o}'");
if (Math.Abs(sourceCount - acumaticaCount) > 0.01 * sourceCount)
    await alert.Send($"Sync mismatch: source={sourceCount}, acumatica={acumaticaCount}");

For the broader reconciliation patterns, see the error handling guide and the bulk loads guide.

Operational hygiene

The integration will need to be maintained. The habits that pay for themselves:

Test on production data

Always test the integration on a copy of production data before going live. Sample data misses the edge cases — a customer with a special character in the name, an invoice in a foreign currency, a record with a null in a required field. Production data has all of these.

For the broader operational patterns, see the CI/CD guide and the monitoring and alerting guide.

Wrapping up

The four steps (pull, transform, push, reconcile), the auth flow, the idempotency, the rate limit, the webhooks, the reconciliation, the runbook. Get all eight right and the integration is invisible. Skip any one and you are debugging at 2 AM. The discipline is not glamorous, but it is what separates an integration that works from an integration that survives.