Genesys Cloud API 401 after refresh - clock skew issue?

I am working on a custom agent desktop widget using the Embeddable Client App SDK. The app handles its own OAuth token management to keep the UI responsive without relying on the host app’s refresh cycle. I’ve implemented a standard refresh flow.

The issue is this. The widget works fine for a while. Then, right after the access token expires and the refresh token is used, the next API call fails with a 401 Unauthorized. If I wait a few seconds and retry, it works. Or if I force a manual refresh, it works.

I suspect this is a clock skew problem between the server issuing the token and the client making the request. The JWT payload shows an exp time that seems correct, but the Genesys Cloud API gateway is rejecting it as expired.

Here is the refresh logic in my service:

async refreshToken() {
 const response = await fetch('https://api.mypurecloud.com/oauth/token', {
 method: 'POST',
 headers: {
 'Content-Type': 'application/x-www-form-urlencoded',
 'Authorization': 'Basic ' + btoa(clientId + ':' + clientSecret)
 },
 body: new URLSearchParams({
 'grant_type': 'refresh_token',
 'refresh_token': this.refreshToken
 })
 });
 
 if (!response.ok) throw new Error('Refresh failed');
 const data = await response.json();
 this.accessToken = data.access_token;
 // I store the expiry time from the token payload
 this.tokenExpiry = new Date(data.expires_in * 1000 + Date.now());
}

And here is how I check before making calls:

async makeApiCall(endpoint) {
 if (Date.now() >= this.tokenExpiry - 5000) {
 await this.refreshToken();
 }
 
 return fetch(`https://api.mypurecloud.com${endpoint}`, {
 headers: {
 'Authorization': 'Bearer ' + this.accessToken
 }
 });
}

The 401 response body is just {"message":"Unauthorized"}. No details on why. Is there a way to handle clock skew in the SDK or do I need to adjust my expiry check logic? I’ve tried adding a buffer, but it feels hacky. The token seems valid when I decode it in jwt.io, but the server says no.

Any ideas on how to debug the actual time difference the server sees?