CXone Webhook to ServiceNow - OAuth token expiration on long-running syncs

Hey everyone,

We’ve got a weird timing issue with our ServiceNow integration on NICE CXone. Basically, the token exchange is hitting a wall during high volume bursts (401). It took like 3 hrs to figure out that the refresh token is being invalidated way too early when the webhook pipeline is hammered.

Here’s the flow of how the data is moving:

[CXone Event] → [Webhook] → [OAuth Middleware] → [ServiceNow Table API]
^
| (Token Refresh if Expired)
v
[OAuth Token Store]

The middleware handles the token swap, but we’re seeing the POST /oauth_token.do call return a “token expired” error even though the timestamp says it should have another 15 mins (400). It’s like the session is being killed on the ServiceNow side before the CXone webhook actually triggers the refresh logic.

The code for the refresh check looks something like this:

if (token.expires_at < Date.now()) {
 const response = await axios.post('https://instance.service-now.com/oauth_token.do', {
 grant_type: 'refresh_token',
 client_id: process.env.CLIENT_ID,
 client_secret: process.env.CLIENT_SECRET,
 refresh_token: token.refresh_token
 });
 return response.data.access_token;
}

The logs show the request going out, but the response comes back as an invalid grant (400). It’s not happening on every call, just during these spikes where we’re pushing maybe 50-100 incidents per minute. The sync just dies for a few seconds then comes back.

Stop fighting the refresh token race condition. It’s a nightmare during bursts.

Fastest fix is to move the token management to a middleware layer or a simple Lambda. Cache the ACCESS_TOKEN in a DATABASE or Redis and only trigger the REFRESH_FLOW when the token is actually 5 minutes from expiring.

{
 "token_expiry": "2023-10-27T10:00:00Z",
 "access_token": "ey..."
}

Don’t let CXone handle the logic directly. It’s too flaky.

3 Likes

The Redis approach is a good call, but be careful with how the cache TTL is set. If it’s too close to the actual token expiration, you’ll just shift the 401s from the refresh cycle to the access cycle during those high volume bursts.

A better way to handle this in CXone is to implement a “grace period” in the middleware. Instead of waiting for the 401 or the exact expiration timestamp, trigger the refresh when the token has maybe 5-10% of its life left.

If the logic is handled in a Node.js middleware, something like this prevents the race condition:

async function getValidToken() {
 const cached = await redis.get('cxone_token');
 const now = Date.now();
 
 // Refresh if token is missing or expires in less than 5 mins
 if (!cached || (cached.expiry - now) < 300000) {
 const newToken = await refreshCXoneToken(); 
 await redis.set('cxone_token', JSON.stringify(newToken), 'EX', newToken.expires_in);
 return newToken.access_token;
 }
 return cached.access_token;
}

Also, just a heads up on the API side- the v1 auth endpoints can be finicky with concurrent requests for the same client ID. If you don’t lock the refresh cess, you’ll end up requesting five new tokens at once during a burst, which is exactly why the refresh token gets invalidated early. Use a distributed lock or a single-threaded worker for the refresh call to keep it clean.