Skip to content
Latchkey

chrome-webstore-upload "invalid_grant" / bad refresh token in CI

chrome-webstore-upload exchanges your client id, client secret, and refresh token for a short-lived access token. "invalid_grant" means Google rejected that exchange, so no upload can proceed.

What this error means

The upload step fails during token exchange with "invalid_grant" or "Token has been expired or revoked", before any package is sent.

chrome-webstore-upload
Error: invalid_grant
{
  "error": "invalid_grant",
  "error_description": "Token has been expired or revoked."
}

Common causes

The refresh token expired or was revoked

Refresh tokens for testing-mode OAuth clients expire, and changing the client or scopes invalidates an old token.

Client id/secret do not match the token

The refresh token was minted for a different OAuth client than the one configured in CI, so the grant is invalid.

How to fix it

Mint a fresh refresh token

  1. Re-run the OAuth consent flow for your Web Store API client to get a new refresh token.
  2. Update the CI secret with the new token and matching client id/secret.
  3. Publish the OAuth client (out of testing mode) so the token does not expire.
.github/workflows/ci.yml
env:
  CLIENT_ID: ${{ secrets.WEBSTORE_CLIENT_ID }}
  CLIENT_SECRET: ${{ secrets.WEBSTORE_CLIENT_SECRET }}
  REFRESH_TOKEN: ${{ secrets.WEBSTORE_REFRESH_TOKEN }}

Keep id, secret, and token from one client

Regenerate all three together so the refresh token always matches the client that issued it.

Terminal
chrome-webstore-upload upload --source dist.zip --extension-id "$EXT_ID"

How to prevent it

  • Use a published (not testing-mode) OAuth client so tokens persist.
  • Store client id, secret, and refresh token together as a matched set.
  • Rotate and re-mint the token before it expires.

Related guides

Tired of flaky CI? Latchkey auto-heals failed jobs and retries them for you. Start free →