Code 124 “Invalid access token” Error when trying to add registrant to meeting

I’ve read the articles I could find on this, and none of them appear to apply to my issue. I’m testing the workflow in Postman before doing any actual development work. I’ve got the following endpoints working fine:

  • Authentication for JWT
  • Create a new meeting
  • List upcoming meetings
  • Fetch meeting details
  • Update meeting details

What’s not working for me is adding a registrant to a meeting, which always gives me a 401 error {“code”:124,”message”:”invalid access token”}.

For the record, manual registration works fine, both through the registration link and via csv import. I’ve also created this on multiple test meetings, some created in the Zoom webUI and others via API call, all give the exact same result.

When I call for JWT, in addition to “access_token” I do get the following:

  "token_type": "bearer",

    "expires_in": 3600,

    "scope": "user:read:list_users:admin user:read:user:admin meeting:read:list_meetings:admin meeting:write:meeting:admin meeting:read:meeting:admin meeting:update:meeting:admin meeting:read:list_registrants:admin meeting:write:registrant:admin meeting:write:batch_registrants:admin meeting:read:registrant:admin meeting:update:registrant_status:admin meeting:read:livestream:admin meeting:read:list_polls:admin meeting:read:poll:admin meeting:read:invitation:admin meeting:write:invite_links:admin meeting:read:past_meeting:admin meeting:read:list_past_instances:admin meeting:read:list_past_participants:admin meeting:read:list_poll_results:admin meeting:read:participant:admin meeting:read:participant_feedback:admin meeting:read:participant_callout:admin meeting:read:alert:admin meeting:read:participant_sharing:admin meeting:read:device:admin meeting:read:risk_alert:admin meeting:read:chat_message:admin meeting:read:list_upcoming_meetings:admin meeting:read:past_qa:admin meeting:read:token:admin meeting:read:ai_companion:admin",

    "api_url": "https://api-us.zoom.us"

All my calls use the authentication flow in Postman to retrieve the token from a variable set by the auth call. So the registration call is using the exact same token as the others, so I know there is nothing wrong with it. After the registration fails, I try https://api.zoom.us/v2/meetings/{{MeetingID}} just to confirm that the token is still good.

Here is a curl export from the Postman call for registrations

curl --location 'https://api.zoom.us/v2/meetings/83927864687/registrants' \
--header 'Authorization: Bearer $TOKEN' \
--header 'Content-Type: application/json' \
--data-raw '{
    "first_name": "Test",
    "last_name": "User",
    "email": "testuser@example.com"
  }'

I’m on a Zoom Workspace Pro plan. Does anyone have any ideas where I’m going wrong or what is going on? BTW, I tried to open a ticket with support, who just responded, “Kindly buzz off and post here.”

1 Like

@siggib can you do a HTTP POST instead?

curl --location --request POST “https://api.zoom.us/v2/meetings/83927864687/registrants”
–header “Authorization: Bearer $TOKEN”
–header “Content-Type: application/json”
–data-raw ‘{
“first_name”: “Test”,
“last_name”: “User”,
“email”: “testuser@example.com”
}’

@siggib and get we confirm that the owner of the token is the host of the meeting as well?

Confirmed, the owner of the token is the meeting host. I am doing POST, not sure why Postman is dropping that from the curl command. Just tested it and POST is the only command it doesn’t show correctly in the curl output, sorry I misssed that when I pasted it in the topic.

I tried to attach a screenshot of postman but apperantly that isn’t allowed and neither is linking a screenshot

@siggib ,

I’m showing your what your full request should look like after after a user oauth redirect URL. Note: don’t use the token as it will not work on your account. This is just verbose example of how the request should look like.

Response from User OAuth Redirect URL

{“access_token”:“eyJzdiI6IjAwMDAwMiIsImFsZyI6IkhTNTEyIiwidiI6IjIuMCIsImtpZCI6IjBhOGY1MWI1LWJiYWItNDQyNS1hMDlhLWExMjQ0YWE3NzM4NyJ9.eyJhdWQiOiJodHRwczovL29hdXRoLnpvb20udXMiLCJ1aWQiOiJwSjhaR2tBRVFzU0w3eHlSaWFaU1ZBIiwidmVyIjoxMCwiYXVpZCI6ImU1OTAwYjk3MTYxYTYxNTM2NzhmNGU4NmUyYmFjNjEwNWEzYjU3MTFlMGRhNWQ3ZDkxNTgyZjdiZWU1YjQzZWEiLCJuYmYiOjE3NzMzMTg1ODEsImNvZGUiOiJoUlFWdk1YdVBXMXVvby1zRnZSUnB1bmtLMTUxbjNBY1EiLCJpc3MiOiJ6bTpjaWQ6aXNWVFh2WkFTYWVoU2RWRGpYMzh5USIsImdubyI6MCwiZXhwIjoxNzczMzIyMTgxLCJ0eXBlIjowLCJpYXQiOjE3NzMzMTg1ODEsImFpZCI6IjlzU19QQjJ5UjgyN25tdHNzZzl3Z1EifQ.TO0eBU6YXwkbU5Ew3piRgSae9ezZ7qyvNQsaokPC”,“token_type”:“bearer”,“refresh_token”:“eyJzdiI6IjAwMDAwMiIsImFsZyI6IkhTNTEyIiwidiI6IjIuMCIsImtpZCI6IjVhMmI1Y2RmLTc3ZjgtNGQwZS05MWFkLTljOWEyNmEwOGE5YiJ9.eyJhdWQiOiJodHRwczovL29hdXRoLnpvb20udXMiLCJ1aWQiOiJwSjhaR2tBRVFzU0w3eHlSaWFaU1ZBIiwidmVyIjoxMCwiYXVpZCI6ImU1OTAwYjk3MTYxYTYxNTM2NzhmNGU4NmUyYmFjNjEwNWEzYjU3MTFlMGRhNWQ3ZDkxNTgyZjdiZWU1YjQzZWEiLCJuYmYiOjE3NzMzMTg1ODEsImNvZGUiOiJoUlFWdk1YdVBXMXVvby1zRnZSUnB1bmtLMTUxbjNBY1EiLCJpc3MiOiJ6bTpjaWQ6aXNWVFh2WkFTYWVoU2RWRGpYMzh5USIsImdubyI6MCwiZXhwIjoxNzgxMDk0NTgxLCJ0eXBlIjoxLCJpYXQiOjE3NzMzMTg1ODEsImFpZCI6IjlzU19QQjJ5UjgyN25tdHNzZzl3Z1EifQ.jvuo1ueEMyN6zbcml6kNbwWMups5EhyWZS7pRS85nmeu5P7WO”,“expires_in”:3599,“scope”:… too long to paste here…,“api_url”:“``https://api-us.zoom.us``”}

How to craft an auth token using the access token. Do not include any text like “access_token” in the bearer token

curl --location --request POST “https://api-us.zoom.us/v2/meetings/83927864687/registrants” 
–header “Authorization: Bearer eyJzdiI6IjAwMDAwMiIsImFsZyI6IkhTNTEyIiwidiI6IjIuMCIsImtpZCI6IjBhOGY1MWI1LWJiYWItNDQyNS1hMDlhLWExMjQ0YWE3NzM4NyJ9.eyJhdWQiOiJodHRwczovL29hdXRoLnpvb20udXMiLCJ1aWQiOiJwSjhaR2tBRVFzU0w3eHlSaWFaU1ZBIiwidmVyIjoxMCwiYXVpZCI6ImU1OTAwYjk3MTYxYTYxNTM2NzhmNGU4NmUyYmFjNjEwNWEzYjU3MTFlMGRhNWQ3ZDkxNTgyZjdiZWU1YjQzZWEiLCJuYmYiOjE3NzMzMTg1ODEsImNvZGUiOiJoUlFWdk1YdVBXMXVvby1zRnZSUnB1bmtLMTUxbjNBY1EiLCJpc3MiOiJ6bTpjaWQ6aXNWVFh2WkFTYWVoU2RWRGpYMzh5USIsImdubyI6MCwiZXhwIjoxNzczMzIyMTgxLCJ0eXBlIjowLCJpYXQiOjE3NzMzMTg1ODEsImFpZCI6IjlzU19QQjJ5UjgyN25tdHNzZzl3Z1EifQ.TO0eBU6YXwkbU5Ew3piRgSae9ezZ7qyvNQsaokPC” 
–header “Content-Type: application/json” 
–data-raw ‘{
“first_name”: “Test”,
“last_name”: “User”,
“email”: “testuser@example.com”
}’


If this looks similar on your end on what you are doing, we will need to look at how you are getting the access token. Here’s a sample code in nodejs. request needs to be sent to https://zoom.us/oauth/token with grant_type as authorization_code

app.get(‘/redirecturlformsdkoauth’, async (req, res) => {
const code = req.query.code;

if (!code) {
  return res.status(400).json({ error: 'Missing code' });
}

try {
  const response = await axios.post(
    'https://zoom.us/oauth/token',
    new URLSearchParams({
      code,
      grant_type: 'authorization_code',
      redirect_uri: 'https://example.com/redirecturlformsdkoauth',
    }).toString(),
    {
      headers: {
        Authorization: `Basic ${Buffer.from(
          `${process.env.ZOOM_CLIENT_ID_NEW}:${process.env.ZOOM_CLIENT_SECRET_NEW}`
        ).toString('base64')}`,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    }
  );

  return res.status(200).json(response.data);
} catch (error) {
  console.error(error.response?.data || error.message);
  return res.status(500).json({ error: 'OAuth token exchange failed' });
}

});

Yes, mine is exactly like that. Here is the exact output for me

{
"access_token": "eyJzdiI6IjAwMDAwMiIsImFsZyI6IkhTNTEyIiwidiI6IjIuMCIsImtpZCI6IjQ3MGI3MDliLTQ5MWEtNGE1ZS1iOTE2LWU4ZmE5YzRiYzQ2NyJ9.eyJhdWQiOiJodHRwczovL29hdXRoLnpvb20udXMiLCJ1aWQiOiJudGZTLWlZVlJWbS1ybW9OeTVvZldBIiwidmVyIjoxMCwiYXVpZCI6IjYwYWRiMzIzZGFlMTgxNzQzOWE1NTEwMTEwNjUyZDk1NmZjZTdhOGFjNDFiNmY5MzQ3Yjk0ODQyNTUyMzE4NGYiLCJuYmYiOjE3NzMzMjU5NDAsImNvZGUiOiJ2d1BqTVdwX1JnaU5WeVdzd1o1Rjl3dHI0NVV4QjBnREEiLCJpc3MiOiJ6bTpjaWQ6U0lNUDNkdWVRMzZlVDJOdWFCY2NydyIsImdubyI6MCwiZXhwIjoxNzczMzI5NTQwLCJ0eXBlIjozLCJpYXQiOjE3NzMzMjU5NDAsImFpZCI6ImRLR1BsekQ0UWEtakJ6dElQT0dGYUEifQ.1_h0bWUs_SwUCQzDVAumBzQLA-tdfHP7A8VAMFxO9a4F7V94KtQ9himwx2r7U9SOGTbf8CBrbYMcugxKrYYQYQ",
"token_type": "bearer",
"expires_in": 3599,
"scope": "user:read:list_users:admin user:read:user:admin meeting:read:list_meetings:admin meeting:write:meeting:admin meeting:read:meeting:admin meeting:update:meeting:admin meeting:read:list_registrants:admin meeting:write:registrant:admin meeting:write:batch_registrants:admin meeting:read:registrant:admin meeting:update:registrant_status:admin meeting:read:livestream:admin meeting:read:list_polls:admin meeting:read:poll:admin meeting:read:invitation:admin meeting:write:invite_links:admin meeting:read:past_meeting:admin meeting:read:list_past_instances:admin meeting:read:list_past_participants:admin meeting:read:list_poll_results:admin meeting:read:participant:admin meeting:read:participant_feedback:admin meeting:read:participant_callout:admin meeting:read:alert:admin meeting:read:participant_sharing:admin meeting:read:device:admin meeting:read:risk_alert:admin meeting:read:chat_message:admin meeting:read:list_upcoming_meetings:admin meeting:read:past_qa:admin meeting:read:token:admin meeting:read:ai_companion:admin",
"api_url": "https://api-us.zoom.us"
}

The curl code I’m using:

curl --location --request POST 'https://zoom.us/oauth/token?grant_type=account_credentials&account_id={redacted}' \
--header 'Authorization: Basic {redacted}' \
--header 'Content-Type: application/x-www-form-urlencoded'

Please note I am using grant_type=account_credentials, when I try grant_type=authorization_code, I get an error 400 invalid request error. Also note that the token works for meeting create, meeting details, update meeting and list meetings. Does registration require a different type of token than all the other end points?

@siggib grant_type=account_credentials is used for S2S OAuth. User OAuth uses grant_type=authorization_code to exchange for access token

When the callback happens, you will get a code in the query string, this code query string is used to exchanged for an user oauth access token.

you must provide the correct credentials, and the same identical redirectUri as specific in marketplace.zoom.us settings, the code which was in the query string.

If you are using grant_type=account_credentials, chances are the token is a S2S token and not the User OAuth token.

const credentials = Buffer.from(${clientId}:${clientSecret}).toString(‘base64’);
const redirectUri = 'https://nodejs.asdc.cc/redirecturlforoauth?type='+type;

log('DEBUG', 'Exchanging OAuth code', {
  requestId: req.requestId,
  redirectUri,
  clientIdPrefix: clientId ? clientId.substring(0, 8) + '...' : 'MISSING',
  hasClientSecret: !!clientSecret
});

const response = await axios.post('https://zoom.us/oauth/token',
  new URLSearchParams({
    code: code,
    grant_type: 'authorization_code',
    redirect_uri: redirectUri
  }).toString(),
  {
    headers: {
      'Authorization': `Basic ${credentials}`,
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  }
);

log('INFO', 'OAuth token exchange success', { requestId: req.requestId });

res.status(200).json(response.data);

@siggib meeting:read:meeting:admin seems to be an admin oauth scope, could you check if the marketplace app is user oauth or admin oauth?

@chunsiong.zoom I’m sorry I seem to have neglected to state that I’m working on an S2S app, not a UserApp; my apologies for that oversight. How does that change the situation?

How do I check if the app is user oauth or admin oauth. I’m assuming that since it is an S2S app, that it’s admin oAuth, but I hate to assume.

Hi @siggib ,

To check what is the type of app. you will need to go to marketplace.zoom.us, to check out what is type of app.

If you are using S2S, it is fairly straight forward, no redirect around at all. Just do something like this

You will use your key and secret encoded in base64 to get the access token. Grant type is account_credentials, remember to include your account ID when making the request.

app.get(‘/s2soauth’, async (req, res) => {
try {
const clientId = process.env.ZOOM_S2S_CLIENT_ID;
const clientSecret = process.env.ZOOM_S2S_CLIENT_SECRET;
const accountId = process.env.ZOOM_S2S_ACCOUNTID;

log('DEBUG', 'S2S OAuth request', {
  requestId: req.requestId,
  hasClientId: !!clientId,
  hasClientSecret: !!clientSecret,
  hasAccountId: !!accountId
});

const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
const url = `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${accountId}`;

const response = await axios.post(url, null, {
  headers: {
    'Authorization': `Basic ${credentials}`,
    'Content-Type': 'application/x-www-form-urlencoded'
  }
});

log('INFO', 'S2S OAuth success', { requestId: req.requestId });
res.status(200).json(response.data);

} catch (error) {
log(‘ERROR’, ‘S2S OAuth failed’, {
requestId: req.requestId,
error: error.message,
response: error.response?.data
});
res.status(500).json({ error: error.message });
}
});


@chunsiong.zoom

Here is what the marketplace says about my app

FooEvents

Intend to publish: No Account-level app Server-To-Server OAuth

I’ve got the authentication flow set up as you said, and the token works great for

  • GET v2/users/me/meetings?type=upcoming (meeting list)
  • GET v2/meetings/{{MeetingID}} (Meeting details)
  • PATCH v2/meetings/{{MeetingID}} (Meeting update)
  • POST v2/users/me/meetings (New Meeting)

The only issue I am having is with POST v2/meetings/{{MeetingID}}/registrants I get a 401 Unauthorized {“code”: 124,“message”: “Invalid access token.”} on that one.

When I do a GET v2/meetings/{{MeetingID}} immediately after, I get the meeting details. MeetingID and token are both Postman variables, and both calls use the same bearer-type authentication configured by Postman, referencing the same variable. So there is no risk of typos in this flow.

Why would I get an invalid token on POST v2/meetings/{{MeetingID}}/registrants when that exact same token works great for GET v2/meetings/{{MeetingID}}

It goes without saying that all those calls are against https://api.zoom.us/

@siggib can you double check on the authorization tab within postman, it is the same auth type?

In postman, unless you specify it, there is a default auth (Oauth 2.0) which might not be the S2S token

@chunsiong.zoom Since I am not allowed to attach images or include links, it’s hard to share screenshots so that you can see that my Postman is set up correctly.

I’m not using the oAuth feature in Postman; I’m manually fetching a token using a xyz:zoom.us/oauth/token?grant_type=account_credentials&account_id={{AccountID}} (I replaced https:// with xyz: to prevent this editor from turning this into a link and then refusing to post this because I’m including a link).

Then this script stores the cookie in a variable.

const json = pm.response.json();
   pm.collectionVariables.set("zoom_token", json.access_token);

Then I use the Postman Bearer Authentication mechanism, referencing that variable, to authenticate each subsequent call. As I stated a few times now, this works great for

  • GET v2/users/me/meetings?type=upcoming (meeting list
  • GET v2/meetings/{{MeetingID}} (Meeting details)
  • PATCH v2/meetings/{{MeetingID}} (Meeting update)
  • POST v2/users/me/meetings (New Meeting)

And I ask again, does v2/meetings/{{MeetingID}}/registrants require a different authentication flow from v2/meetings/{{MeetingID}} (or any of the other flows I mentioned above that work)? If not, why am I getting an invalid token error on v2/meetings/{{MeetingID}}/registrants for a token that works great on v2/meetings/{{MeetingID}}

If it does require a different authentication flow, exactly how is it different?

If the other endpoints are working but adding a registrant returns code 124, it’s often related to the token type or scopes being used for that specific endpoint. Zoom has been phasing out JWT apps, and some registrant-related endpoints may not work properly with them anymore.

You might want to try generating the token using a Server-to-Server OAuth app instead and test the same request in Postman. Also double-check that you’re passing the token in the header as:

Authorization: Bearer {access_token}

and that the meeting you’re using has registration enabled, since the registrant endpoint requires it.

Testing with Server-to-Server OAuth usually resolves this issue.

I’m not sure I follow, my app is S2S already, here is what it says on the first app screen in the app marketplace: Intend to publish: No Account-level app Server-To-Server OAuth.
Yes the meeting has registration enabled and I’ve confirmed that by manually registering. Also all of them “use Authorization: Bearer {access_token}”

Can you please clarify what you mean? What scope could I be missing? Also what should I do different to test with S2S oAuth when my app already is S2S oAuth?
here again is my auth call to fetch the token https://zoom.us/oauth/token?grant_type=account_credentials&account_id={{AccountID}}
and it returns this scope
“scope”: “user:read:list_users:admin user:read:user:admin meeting:read:list_meetings:admin meeting:write:meeting:admin meeting:read:meeting:admin meeting:update:meeting:admin meeting:read:list_registrants:admin meeting:write:registrant:admin meeting:write:batch_registrants:admin meeting:read:registrant:admin meeting:update:registrant_status:admin meeting:read:livestream:admin meeting:read:list_polls:admin meeting:read:poll:admin meeting:read:invitation:admin meeting:write:invite_links:admin meeting:read:past_meeting:admin meeting:read:list_past_instances:admin meeting:read:list_past_participants:admin meeting:read:list_poll_results:admin meeting:read:participant:admin meeting:read:participant_feedback:admin meeting:read:participant_callout:admin meeting:read:alert:admin meeting:read:participant_sharing:admin meeting:read:device:admin meeting:read:risk_alert:admin meeting:read:chat_message:admin meeting:read:list_upcoming_meetings:admin meeting:read:past_qa:admin meeting:read:token:admin meeting:read:ai_companion:admin”,

@siggib can you do 2 things for me?

  1. For the failed response, can you provide me with a zm-x-tracking ID which is in the response header?

  2. Another thing to isolated the issue from postman, can you manually run the query from your terminal / commandline?

In postman, every single endpoint has their own authorization. I’m trying to eliminate the possiblity of postman using a different AUTH token compared to the working endpoints.

GET v2/users/me/meetings?type=upcoming (meeting list)
GET v2/meetings/{{MeetingID}} (Meeting details)
PATCH v2/meetings/{{MeetingID}} (Meeting update)
POST v2/users/me/meetings (New Meeting)

Could you try this below with your access token in your terminal / cli?

curl -X POST “https://api.zoom.us/v2/meetings/83927864687/registrants”
-H “Authorization: Bearer YOUR_ACCESS_TOKEN”
-H “Content-Type: application/json”
-d ‘{
“first_name”: “Test”,
“last_name”: “User”,
“email”: “testuser@example.com”
}’

Thank you for your patience and your help in tracking this down. It did in deed work from the CLI, so something is up with Postman. I’ll take it from here. Thanks again for all your help.