🔗 Zoom Meeting Integration in a FileMaker-based CRM System

Integrating Zoom meeting functionality into a FileMaker-based CRM system opens up a streamlined workflow for scheduling, inviting, and following up on virtual meetings — all from within your custom app. This guide outlines how I built a seamless integration using the ACF plugin, FileMaker scripting, and the Zoom API.

I used the Email-AI demo app as a starting point for this application – as it allready have all the markdown to html and e-mail sending logic in place. (This app is available in our download area)


🚀 Step-by-Step Integration

1. 🧰 Prepare Your Zoom App

Before you can talk to the Zoom API, you need to create an OAuth app via the Zoom Marketplace:

  • Choose Server-to-Server OAuth.
  • Register the app, define scopes (like meeting:write and user:read), and generate credentials.
  • Copy the Account ID, Client ID, and Client Secret for later use.

2. ⚙️ Storing Zoom Credentials in FileMaker

In FileMaker, I created a Preferences table where each Zoom credential is stored in dedicated fields:

  • Zoom_Account_ID
  • Zoom_Client_ID
  • Zoom_Client_Secret

These can be set via an admin-only layout and securely referenced by scripts and ACF functions.


3. 🔐 Retrieving an Access Token

The first ACF function handles the authentication with Zoom and retrieves an access token:

function Zoom_GetToken (string account_id, string client_id, string client_secret)

    string url = "https://zoom.us/oauth/token?grant_type=account_credentials&account_id=" + account_id;

    string auth = "Authorization: Basic " + Base64_Encode(client_id + ":" + client_secret,1);
    string header = "Content-Type: application/x-www-form-urlencoded";

    string result = HTTP_POST (url, "", header, auth);
    if (http_status_code != 200) then
        throw format("HTTP status code error %d: , message: %s", http_status_code, result); 
    end if
    if ( left(result, 1) != "{") then
        throw "Endpoint did not return a JSON: " + result; 
    end if

    JSON  js = result;

    timestamp exp = now()+int(js["expires_in"]); 
    js ["expTime"] = long(exp); 
    js ["expTS"] = exp; 

    return  js; 

end

This token is required for all subsequent API calls and typically expires after 1 hour.


4. 🔁 Reusing or Refreshing the Token

To avoid repeated authentication calls, I wrote a second ACF function that checks if a valid token already exists in a global FileMaker variable ($$ZoomToken). If not, it triggers a fresh login:

function ZoomFetchToken (string Account)
  string storedToken = $$ZoomToken;
  JSON js ;
  bool ok;
  case
      :(storedToken == "")
      :(left(storedToken, 1) != "{")
      default
          js = storedToken;
          if (long(js ["expTime"])>long(now())+30) then
              ok = true;
          end if
  end case
  if ( ok) then
      return js;
  end if
  print "Zoom token expired or missing. Fetching new one.\n";

  array string aAccID, aCliID, aCliSec;
  string sql = "SELECT ZoomAppID, ZoomClientID, ZoomSecret FROM Email_Accounts
  WHERE ShortName = :Account
  INTO :aAccID, :aCliID, :aCliSec";
   
  string res = ExecuteSQL (sql );
  if ( sizeOf(aCliID) == 0) then
  throw "Did not find account: " + Account + "\n" + res;  
  end if

  js = Zoom_GetToken(aAccID[1], aCliID[1], aCliSec[1]);
  $$ZoomToken = string(js);
  return js;
end

5. 🗃️ Fields Required for Zoom Meeting Integration

I extended the existing Email or Meetings table with these fields:

  • Zoom_Topic
  • Zoom_StartTime
  • Zoom_Duration
  • Zoom_StartURL
  • Zoom_JoinURL
  • Zoom_MeetingID
  • Zoom_Response_JSON

6. 📅 Creating a Zoom Meeting from FileMaker

This ACF function uses the access token to call the Zoom API and create a scheduled meeting:

function Zoom_CreateMeeting (string topic, string start, int duration, string Instant)

timestamp startTS = start;

  JSON tokenJS = ZoomFetchToken(Email_Messages::Email_Account);
  if (tokenJS["access_token"] == "?") then
  throw "Could not get access token from ZOOM api";
  end if
  string token = tokenJS["access_token"];
  string auth = "Authorization: Bearer " + token;
  string header = "Content-Type: application/json";
  int meetingtype = (Instant == "Yes")?1:2;

  JSON payload = JSON(
      "topic", topic,
      "type", meetingtype,
      "start_time", string(makeUTCtime(startTS), "%Y-%m-%dT%H:%M:%SZ"), // UTC
      "duration", duration,
      "timezone", "Europe/Oslo",
      "agenda", "Agenda sent by email",
      "settings.join_before_host", false,
      "settings.waiting_room", true
  );

  string url = "https://api.zoom.us/v2/users/me/meetings";
  string result = HTTP_POST(url, string(payload), header, auth);
  JSON js = result;

  if (http_status_code != 201) then
      throw format("Zoom error %d: %s", http_status_code, result);
  end if

  return js;
end

The result contains all needed info, including join and start links, meeting ID, and password.


7. 📎 Creating an ICS Calendar Attachment

To attach a .ics calendar file to the email, I use this ACF function:

function MakeICS_v2 (
  string uid, timestamp dtstart, int durmin,
  string summary, string description, string location,
  string fromEmail, ARRAY string invitees
)

string s_dtstart = string (makeUTCtime ( dtstart), "%Y%m%dT%H%M%SZ") ;
timestamp dtend = dtstart + durmin*60;
string s_dtend = string (makeUTCtime (dtend), "%Y%m%dT%H%M%SZ");
  string ics = "BEGIN:VCALENDAR\n", email, name;
  ics += "VERSION:2.0\n";
  ics += "PRODID:-//HORNEKS//MeetAdmin//EN\n";
  ics += "METHOD:REQUEST\n";
  ics += "BEGIN:VEVENT\n";
  ics += "UID:" + uid + "\n";
  ics += "DTSTAMP:" + string(now(0), "%Y%m%dT%H%M%SZ") + "\n";
  ics += "DTSTART:" + s_dtstart + "\n";
  ics += "DTEND:" + s_dtend + "\n";
  ics += "SUMMARY:" + EscStr(summary) + "\n";
  ics += "DESCRIPTION:" + EscStr(description) + "\n";
  ics += "LOCATION:" + EscStr (location) + "\n";
  ics += "ORGANIZER;CN=Organizer:mailto:" + fromEmail + "\n";
  int i;
  for (i = 1, sizeof(invitees))
  name = invitees[i];
  email = between(name, "<", ">");
  if (email == "") then
  email = name;
  name = "Invitee"+string(i);
  else
  name = substitute (name, "<"+email+">", "");
  name = trimboth(name);
  if (name == "") then // only brackeded e-mail
  name = "Invitee"+string(i);
  end if
  end if
      ics += format ("ATTENDEE;CN=%s;RSVP=TRUE:mailto:%s\n", EscStr(name), email);
  end for

// Add reminder 30 minutes before
  ics += "BEGIN:VALARM\n";
  ics += "TRIGGER:-PT30M\n";
  ics += "ACTION:DISPLAY\n";
  ics += "DESCRIPTION:Reminder\n";
  ics += "END:VALARM\n";

  ics += "END:VEVENT\n";
  ics += "END:VCALENDAR\n";

  string outpath = temporary_directory() + "zoom_inv_"+@Get(UUID)@+".ics";
  // Save file
  ics = substitute (ics,"\n", "\r\n");
  string res = FilePutContent(outPath, ics);
  return outpath;
end

Since we already have the Zoom response stored, we can make a MakeICS function that pulls everything from that request. Then it calls the function above with all its eight parameters.

function MakeICS ( string jsonResponse )

	JSON js = jsonResponse;
	string id = js["id"]; 
	string uid = "zoom-" + id + "@horneks.no";
	timestamp dtstart = timestamp(js["start_time"]);
	int seconds = long(now()) - long(now(0)); 
    dtstart = dtstart + seconds; 
	int durmin = int(js["duration"]);
	string summary = js["topic"];
	string join_url = js["join_url"];
	string description = "Click the link to join Zoom:\n" + join_url;
	string location = "Online";
	string fromEmail = js["host_email"];

	// Extract attendees from FileMaker fields
	ARRAY string invitees, inv;
	int i; 
	string em; 

	macro AddEmailsToList ( string emlist )
		clear(inv); 
		inv = explode (",", emlist); 
		for (i= 1, sizeOf(inv))
			em = trimboth(inv[i]); 
			if (em!="") then
				invitees[] = em; 
			end if
		end for
	end macro

	AddEmailsToList (Email_Messages::MailTo); 
	AddEmailsToList (Email_Messages::Mailcc); 
	AddEmailsToList (Email_Messages::Mailbcc); 
	

	print "inviterte: " + implode(",", invitees); 

	// Pass to original MakeICS core implementation
	string path =  MakeICS_v2(uid, dtstart, durmin, summary, description, 
                          location, fromEmail, invitees);
	string primkey = Email_Messages::PrimaryKey; 

	// We have the file path, let us add as attachement to the mail: 
	string sql = "INSERT INTO Email_Attachements (fk_EmailMessage, FilePath) VALUES (:primkey, :path)"; 
	string res = ExecuteSQL ( sql);
	return res; 
end

It takes the Zoom API response and generates a valid calendar invitation, which is added to the related attachments table.


8. 📨 Embedding Meeting Info in the Email Body

To enrich the email message, this function formats the Zoom details as a Markdown table and inserts it after the greeting:

function InsertZoomInviteBlock(string jsonResponse, string markdownBody, string language)

  JSON js = jsonResponse;

  string joinURL = js["join_url"];
  string topic = js["topic"];
  timestamp startTime = timestamp(js["start_time"]);
  int seconds = long(now()) - long(now(0));
  starttime = starttime + seconds;
  int dur = int(js["duration"]);
  string timeStr = string(startTime, "%A %d %B %Y at %H:%M");
  string host = js["host_email"];
  string password = js["password"];
  string inviteBlock;
if (language == "NO") then
  inviteBlock = "\n\n##📅 Invitasjon til videomøte på Zoom\n";
else
inviteBlock = "\n\n##📅 Zoom Video-Meeting Invitation\n";
end if
inviteBlock += "|   |   |\n";
inviteBlock += "|:----|:----|\n";
if (language == "NO") then
inviteBlock += "| **Emne:** | " + topic + " |\n";
inviteBlock += "| **Tid:** | " + timeStr + " |\n";
inviteBlock += "| **Varighet:** | " + string(dur) + " minutter |\n";
inviteBlock += "| **Møtevert:** | " + host + " |\n";
inviteBlock += "| **Møte lenke:** | [" + joinURL + "](" + joinURL + ") |\n";
inviteBlock += "| **Møte passord:** | `" + password + "` |\n\n";
else
inviteBlock += "| **Topic:** | " + topic + " |\n";
inviteBlock += "| **Time:** | " + timeStr + " |\n";
inviteBlock += "| **Duration:** | " + string(dur) + " minutes |\n";
inviteBlock += "| **Host:** | " + host + " |\n";
inviteBlock += "| **Join Link:** | [" + joinURL + "](" + joinURL + ") |\n";
inviteBlock += "| **Meeting Password:** | `" + password + "` |\n\n";
end if
  // Split markdown into lines
  markdownBody = substitute (markdownBody, "\r\n", "\n");
  markdownBody = substitute (markdownBody, "\r", "\n");
   
  ARRAY string lines = explode("\n", markdownBody);
  ARRAY string newlines;

  int i;
  for (i = 1, sizeof(lines))
      if (i == 2) then
          newlines[] = inviteBlock;
      end if
      newlines[] = lines[i];
  end for

  return implode("\n", newlines);
end

This lets users preview the full email before sending — complete with topic, time, links, and password.


9. 🧵 Tying It All Together with a Script

A FileMaker script glues everything:

  1. Pulls values from the current email record.
  2. Calls the Zoom creation function.
  3. Generates the ICS file.
  4. Adds the invite block to the Markdown body.
  5. Saves everything and returns control to the user.

This lets users click a single “Create Zoom Meeting” button and preview the fully populated email before sending.


💌 Sample Email Output

Here’s a sample of what a recipient might see:

The .ics attachment ensures that the invitation is added to participants’ calendars with a reminder.


✅ Conclusion

This Zoom integration turns FileMaker into a fully capable meeting management platform. With ACF’s flexibility and FileMaker’s UI, users can schedule, invite, and follow up on Zoom meetings without ever leaving the app.

If you’d like to implement something similar or have questions about ACF or Zoom integration — feel free to reach out!

You may also like...