Acumatica · Integration

Acumatica Zoom Meetings Integration — A Complete Guide

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

John Kihiu12 min read

The ask behind every Zoom–Acumatica project I've scoped is the same: the sales or support team lives in Acumatica's CRM screens — Opportunities, Cases, Activities — and every customer call means alt-tabbing to Zoom, creating a meeting, and pasting the join link back into an activity by hand. Half the time the link never makes it back, and three months later nobody can prove the demo happened. The integration goal is small and concrete: create the meeting from the Acumatica record, store the join URL on it, and log the meeting outcome automatically.

Zoom's auth model: Server-to-Server OAuth

First decision, and Zoom made it for us: JWT apps were retired in 2023, so anything you find referencing JWT credentials is dead documentation. For a backend integration where Acumatica acts on behalf of your own Zoom account, the right app type is Server-to-Server OAuth. You create it in the Zoom App Marketplace (it never gets published — it's private to your account), grant it scopes like meeting:write:admin, and you get an Account ID, Client ID, and Client Secret.

Token acquisition is a single call — no user consent screen, no refresh token dance:

HTTP · S2S TOKEN REQUEST
POST /oauth/token?grant_type=account_credentials&account_id={accountId} HTTP/1.1
Host: zoom.us
Authorization: Basic base64(client_id:client_secret)

HTTP/1.1 200 OK
{ "access_token": "eyJ...", "token_type": "bearer", "expires_in": 3599 }

Tokens last an hour; cache one and refresh on a 401 or a timer. Note that each token request invalidates nothing — but there's a limit on token requests per hour, so don't request a fresh token per API call.

Creating the meeting from Acumatica

I implement this as a custom action on the graph — a "Schedule Zoom Meeting" button on the Opportunity or Case screen via a graph extension. The action gathers what it needs from the record, calls Zoom's POST /v2/users/{userId}/meetings, and writes the results back. The userId is the meeting host's Zoom email — which raises the first mapping question: whose meeting is it? I map the Acumatica record owner's email to the Zoom user; if your Zoom accounts and Acumatica users share the corporate email domain, this is free. If not, you need a cross-reference, and you should sort that out before writing code.

C# · CUSTOM ACTION ON THE CASE GRAPH
public PXAction<CRCase> scheduleZoom;
[PXButton, PXUIField(DisplayName = "Schedule Zoom Meeting")]
protected virtual IEnumerable ScheduleZoom(PXAdapter adapter)
{
    var row = Base.Case.Current;
    if (row == null) return adapter.Get();

    PXLongOperation.StartOperation(Base, delegate
    {
        var body = new {
            topic = "Case " + row.CaseCD.Trim() + " — " + row.Subject,
            type = 2,                              // scheduled meeting
            start_time = DateTime.UtcNow.AddHours(1).ToString("yyyy-MM-ddTHH:mm:ssZ"),
            duration = 45,
            timezone = "Africa/Nairobi",
            settings = new { waiting_room = true, join_before_host = false }
        };
        var meeting = ZoomClient.CreateMeeting(hostEmail, body); // POST /users/{id}/meetings

        // write join_url + meeting id back as an activity on the case
        var tAct = PXGraph.CreateInstance<CRTaskMaint>();
        var act = tAct.Tasks.Insert();
        act.Subject = "Zoom: " + row.Subject;
        act.Body = "Join URL: " + meeting.JoinUrl;
        tAct.Tasks.Cache.SetValueExt(act, "RefNoteID", row.NoteID);
        tAct.Save.Press();
    });
    return adapter.Get();
}

Two implementation notes. PXLongOperation keeps the HTTP round-trip off the UI thread — Zoom is fast, but never make a user's save wait on someone else's API. And store the numeric meeting id (not just the join URL) in a custom field or the activity body; you need it to correlate webhooks later.

The return path: Zoom webhooks into Acumatica

Creating meetings is half the value. The other half is knowing what happened. Zoom's event subscriptions (configured on the same S2S app) will POST you meeting.started, meeting.ended, and — the genuinely useful one — meeting.participant_joined. My relay endpoint handles meeting.ended by looking up the stored meeting ID and updating the Acumatica activity with the actual duration and attendance via the contract-based REST API. Sales managers stop asking "did the demo actually run?" because the activity says so.

Zoom webhook endpoints must answer their URL validation challenge: on subscription, Zoom sends a endpoint.url_validation event containing a plainToken, and you must respond with that token plus its HMAC-SHA256 hash using your webhook secret token. Miss this and the subscription simply won't activate. Validate the x-zm-signature header on every subsequent event, and treat delivery as at-least-once — key your activity updates on meeting ID so a duplicate meeting.ended doesn't create a duplicate log entry.

Don't poll the meetings list

I've seen an integration that polled GET /users/{id}/meetings every minute to detect ended meetings. Zoom's rate limits are per-account, tiered by endpoint, and that poller starved the meeting-creation calls during busy hours. The webhook path costs nothing and is near-real-time.

What I deliberately leave out

Recordings sync (pulling cloud recording links into Acumatica) is a common stretch goal — it works via the recording.completed webhook, but recordings raise retention and privacy questions that are policy problems, not code problems; I make the client answer those first. Calendar two-way sync is the other trap: if the client uses Google Workspace or Microsoft 365, the meeting invite should usually come from the calendar system (with Zoom as the conferencing provider) rather than from Acumatica directly — otherwise you've built a second, worse calendar. Acumatica creates the meeting and logs it; the calendar owns the humans' schedules.

Wrapping up

The productive core of a Zoom integration is small: a Server-to-Server OAuth app, one custom action that creates the meeting and files the join URL as an activity, and a webhook handler that closes the loop when the meeting ends. Map record owners to Zoom hosts via email before you start, keep API calls inside PXLongOperation, answer Zoom's URL validation handshake correctly, and resist the urge to rebuild the calendar. Ship that and the sales team's alt-tab ritual is gone within a sprint.

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.