🔗 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:writeanduser: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_IDZoom_Client_IDZoom_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_TopicZoom_StartTimeZoom_DurationZoom_StartURLZoom_JoinURLZoom_MeetingIDZoom_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:
- Pulls values from the current email record.
- Calls the Zoom creation function.
- Generates the ICS file.
- Adds the invite block to the Markdown body.
- 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!
