I am using the In-Meeting Controls API to programmatically invite a participant to an ongoing live meeting via the participant.invite method.
The API successfully triggers the invitation workflow when the payload contains a Company Contact (a user belonging to the same Zoom account organization). However, when providing an External Contact (it is added as the users contact, but when using the list contacts API these contacts are returned only when explicitly requesting for external contacts), the API rejects the request and fails to send the invitation. I have verified the details of the contacts and the structure of the request body that is sent and it is valid.
Error?
When executing the request with an external contact, the API returns the following error payload:
{
"code": 300,
"message": "No valid contacts were found. Make sure the contacts can be resolved and belong to the same account as the meeting host."
}
The language in the documentation could be clearer so that added external contacts are not interpreted as “a part of the meeting host’s account”. I’ve drafted up a feature request for you, but in the interim, here are your options:
Option 1 — Pre-register the external user (recommended)
Use the Add Meeting Registrant API before the meeting starts. The registrant receives a personal join URL via email, which they can use to join when invited.
Retrieve the meeting’s join_url via GET /v2/meetings/{meetingId} and distribute it to the external user through your own notification channel (email, Team Chat, etc.). This bypasses the API invite restriction entirely. You can pair this with the meeting.startedwebhook to create a custom automation or use Zoom Workflows to create a plain english one. Below is the JSON for the sample Zoom Workflow I created that you can import into your own and modify for your needs:
{
"version": 1,
"exportedAt": "2026-07-15T17:03:08.466Z",
"data": {
"metadata": {
"flowBookId": "",
"name": "Meeting Join Link Sender",
"description": "When a meeting starts, this workflow sends the meeting join link to external contacts.",
"source": "cui",
"sessionId": "" // REDACTED
},
"flows": [
{
"workflow_metadata": {
"flow_id": "",
"name": "Meeting Join Link Sender",
"description": "When a meeting starts, this workflow sends the meeting join link to external contacts."
},
"nodes": [
{
"node_id": "zm.meeting.meeting.meeting_event",
"reference_id": "EFTEST",
"node_type": "trigger",
"node_name": "Meeting event",
"node_alias": "Meeting Started Trigger",
"node_desc": "Start when a meeting event occurs, such as meeting start, end, or other activities.",
"input_map": {
"filter_type": "all_meetings",
"time_range_filter": {}
},
"config": {
"event_name": "meeting.start",
"event_config": {
"time_offset": 0
}
},
"output_schema": {
"type": "object",
"properties": {
"meeting": {
"description": "the meeting object includes meeting info like meeting topic, meeting host etc",
"type": "meeting",
"required": true
}
}
},
"expanded": false,
"children": {
"nodes": [],
"edges": []
}
},
{
"node_id": "zm.aic.agent.deep_reasoning",
"reference_id": "RADGGY",
"node_type": "deep_reasoning",
"node_name": "deep_reasoning",
"node_alias": "Send Join Link to External Contacts",
"node_desc": "",
"input_map": {
"name": "External Meeting Notifier",
"description": "This agent automatically identifies external contacts when a meeting starts and sends them a polished, email-ready notification containing the meeting join link. It retrieves meeting details from the meeting_started_trigger context, distinguishes internal and external participants based on email domains, and sends the join link only to external attendees. If no external contacts or join link are available, it returns a clear skipped status with the reason.",
"system_prompt": "You are a precise and courteous meeting automation assistant. When a meeting starts, you retrieve meeting details and attendee information from the provided trigger context. You identify external contacts by comparing attendee email domains with the host's domain, excluding internal participants. You then generate a professional, email-ready message that includes a clear subject, a brief greeting, the meeting topic if available, the join link, and a short note that the meeting has started. You send this message only to external contacts using the selected email tool. If no external contacts or join link are available, you return a skipped status with a clear reason. All generated messages must be polished, human-readable, and ready to send without further editing.\n\nUSER-FACING TEXT FORMATTING CONTRACT:\n- Any model-generated text that a workflow shows or sends to a person must be\n ready to read, not a raw dump. This includes chat messages, channel posts,\n email subject/body content, notifications, summaries, reports, document text,\n and action/comment descriptions.\n- Use clear structure: concise opening line, short paragraphs, bullet or\n numbered lists for multiple items, section labels when helpful, and blank\n lines between sections. Include owners, dates, decisions, risks, and action\n items as separate readable lines when present.\n- Match the user's language and requested tone. Preserve facts and meaning.\n Do not output raw JSON, dense comma-separated blobs, markdown tables, or\n unbroken paragraphs unless the user explicitly asks for that format.\n- For email bodies, generate clean email-ready content: useful subject text,\n greeting/sign-off when appropriate, readable paragraphs, and lists for\n action items. If the selected email tool expects HTML, provide semantic HTML;\n otherwise provide well-structured plain text or markdown accepted by the tool.\n- When the task both generates content and sends/posts/shares/saves it through\n a tool, complete that whole flow in this node: generate the readable content,\n then call the selected destination tool (for example Gmail send email) with\n that content and the requested recipients/destination.\n\nZOOM CHAT/CHANNEL FORMATTER REQUIREMENT:\n- When this node drafts, summarizes, or otherwise generates content that will\n be sent to Zoom Chat, Zoom Team Chat, a Zoom Channel, or a Zoom Chat DM, it\n MUST use the `zoom_chat_formatter` skill before outputting or sending the\n message.\n- Required sequence: first generate the message content, then apply\n `zoom_chat_formatter` to beautify it for Zoom Chat syntax/readability, then\n output only the beautified content for the downstream Zoom Chat/Channel send\n step. Do not send the raw model-generated draft to Zoom Chat/Channel.",
"task_prompt": "When the meeting starts, identify the external contacts from the current trigger meeting using {{EFTEST.meeting}}. Use the meeting attendee list and host/domain context to distinguish external contacts from internal participants where possible. Send those external contacts the meeting join link from the current meeting. The message should be polished and email-ready: include a clear subject, a brief greeting, the meeting topic if available, the join link, and a short note that the meeting has started. Do not send to internal participants. If no external contact email addresses or no join link are available, return a clear skipped status with the reason instead of inventing recipients or links.",
"context": {},
"tools": [
{
"name": "zm.system.resource.unified_search",
"description": "Zoom System Utilities - Resource search data node. This data node retrieves relevant Zoom resources such as meetings, team chats, and Zoom Docs using semantic search or precise queries, and outputs the matched content for downstream workflow steps. Use cases include finding past meeting transcripts for summarization, retrieving chat history for context-aware automation, and locating Zoom Docs to extract or analyze specific information.",
"app_key": ""
},
{
"name": "zm.aic.tool.resolve_entity",
"description": "Zoom AI Companion - Resolve Entity tool node. This tool converts human-readable names such as chat channels, user names, or meeting topics into their corresponding system IDs so they can be used in downstream workflow steps. Use cases include resolving a channel name before sending a message, finding a user ID to assign a task or send a notification, and locating a meeting ID within a specific time window for follow-up actions.",
"app_key": ""
},
{
"name": "zm.aic.tool.search_web",
"description": "Zoom AI Companion - Web search action. This tool node performs real-time web searches to retrieve up-to-date information, optionally restricting results to specific domains for targeted research. Use cases include answering open-domain questions with current data, gathering competitive or industry insights from trusted websites, and fact-checking or enriching responses with the latest online information.",
"app_key": "web_search"
},
{
"name": "zm.connector_mcp.zm_gmail.gmail_send_email",
"description": "Gmail - Sends an email via Gmail API using the authenticated user's Google profile display name. This is a tool node.",
"app_key": "zm_gmail"
}
],
"skills": [
{
"name": "zoom_chat_formatter",
"description": "Beautifies model-generated content for Zoom Chat/Channel by converting unsupported markdown syntax into Zoom-compatible equivalents, adding emoji-based section headers, extracting action items, bolding summary sentences, converting implicit enumerations into bullet lists, and splitting dense paragraphs; accepts any plain text or markdown content; use when the user asks to format, reformat, send, or prepare content for Zoom Chat or a Zoom Channel.",
"skill_type": "static",
"skill_path": "zoom_chat_formatter",
"required_tools": []
}
],
"model": "gpt-5-chat-latest",
"max_tokens": 200000,
"max_rounds": 10,
"expected_format": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Sent, skipped, or failed."
},
"sent_count": {
"type": "integer",
"description": "Number of external contacts who were sent the join link."
},
"recipients": {
"type": "array",
"items": {
"type": "string"
},
"description": "External contact email addresses that received the join link."
},
"message_subject": {
"type": "string",
"description": "The subject used for the outgoing message."
},
"summary": {
"type": "string",
"description": "A concise human-readable result summary."
}
},
"required": [
"status",
"sent_count",
"recipients",
"summary"
]
},
"outputs": [
{
"name": "status",
"type": "string",
"description": "Sent, skipped, or failed."
},
{
"name": "sent_count",
"type": "integer",
"description": "Number of external contacts who were sent the join link."
},
{
"name": "recipients",
"type": "array",
"description": "External contact email addresses that received the join link."
},
{
"name": "message_subject",
"type": "string",
"description": "The subject used for the outgoing message."
},
{
"name": "summary",
"type": "string",
"description": "A concise human-readable result summary."
}
],
"output_spec_description": [
{
"name": "status",
"type": "string",
"description": "Sent, skipped, or failed."
},
{
"name": "sent_count",
"type": "integer",
"description": "Number of external contacts who were sent the join link."
},
{
"name": "recipients",
"type": "array",
"description": "External contact email addresses that received the join link."
},
{
"name": "message_subject",
"type": "string",
"description": "The subject used for the outgoing message."
},
{
"name": "summary",
"type": "string",
"description": "A concise human-readable result summary."
}
],
"knowledge": {
"web_urls": ""
},
"max_steps": 20
},
"config": {
"execution_mode": "workflow"
},
"output_schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Sent, skipped, or failed."
},
"sent_count": {
"type": "integer",
"description": "Number of external contacts who were sent the join link."
},
"recipients": {
"type": "array",
"description": "External contact email addresses that received the join link."
},
"message_subject": {
"type": "string",
"description": "The subject used for the outgoing message."
},
"summary": {
"type": "string",
"description": "A concise human-readable result summary."
}
}
},
"expanded": false,
"children": {
"nodes": [],
"edges": []
}
}
],
"edges": [
{
"source_node": "EFTEST",
"target_node": "RADGGY"
}
]
}
]
}
}
If your external participant joins via a room system, the participant.invite.room_system_callout method supports external SIP/H.323 addresses and does not enforce the same-account restriction.
Thanks for replying and providing a bunch of solutions @gianni.zoom!
I tried the ‘Add meeting registrant’ API, but it’s only for paid users. I received this response.
{
"code": 200,
"message": "Only available for paid users: doQJKuCWSuGgtbaQUaXqZQ."
}
I’ll explain my use-case a little more. I’m trying to build an android app that allows users to view their zoom meetings and contacts as a list (both company/external). I wanted to add a feature that allows users to directly ‘video call’ a contact from the list. The way I was doing it was
Starting an instant meeting using the create a meeting API and passing the type as 1, indicating an instant meeting
Then I used the in-meeting controls API to invite the contact to that meeting.
The requirement is that the receiver must receive a zoom call, either as a call screen when on their phone or an incoming call notification when on desktop.
I used this workaround since there was no API that allowed directly video calling a contact.
Creating a workflow or inviting the person via a room system callout are not something that solve my problem. Is there another way to achieve the above?
Also, in the official Zoom app, when I click on ‘Invite’ inside a meeting, and choose any of my contact (including external) it works and calls the person.