# Fix Intercom OAuth refresh failing with invalid_grant

## TL;DR
The Intercom refresh token is dead - expired, revoked, or already consumed by a concurrent refresh - so the token endpoint returns invalid_grant. Get a fresh grant: send the workspace admin back through the Intercom OAuth flow, store the new token pair atomically, and make refresh failure trigger re-auth instead of an infinite retry loop. Also fix concurrent refreshes, which burn single-use refresh tokens.

## The error
```text
POST https://api.intercom.io/auth/eagle/token -> 400
{"error": "invalid_grant", "error_description": "The provided authorization grant is expired or revoked"}
```

## Use this skill
Agent? Search Vectle for this error:
```bash
curl -s "https://vectle.com/api/v1/search?q=intercom%20oauth%20token%20refresh%20failed%20invalid_grant"
```

## Fix it

### Step 1: Confirm invalid_grant is deterministic

```bash
Attempt one refresh and log the exact error body. One attempt is enough; repeated attempts cannot revive a dead grant.
```

Expected: invalid_grant again. Deterministic failure means the grant is dead, not the network.

### Step 2: Stop the refresh loop and flag re-auth

```bash
Set the integration state to needs-reauth and stop all refresh attempts for that workspace.
```

Expected: no more doomed token calls. Every retry of a dead grant is wasted.

### Step 3: Re-authorize through the Intercom OAuth flow

```bash
Direct the workspace admin to the Intercom app's OAuth authorize URL to grant access again.
```

Expected: a fresh authorization code. Only an admin of the Intercom workspace can re-grant.

### Step 4: Exchange and store the new pair atomically

```bash
Exchange the code for the new access and refresh tokens and write both to the credential store in one transaction.
```

Expected: API calls succeed. Atomic storage prevents half-written pairs that fail the same way.

### Step 5: Fix the refresh client

```bash
Add single-flight refresh (one in-flight refresh per workspace; concurrent callers wait for it) and make invalid_grant flip the integration to needs-reauth with an admin notification.
```

Expected: concurrent refreshes stop burning tokens, and the next death pages a human with a clear action.

## When this applies

- Intercom refresh returns invalid_grant consistently
- The integration worked before and suddenly lost auth
- Failures follow a deploy that added concurrent workers

## When it doesn't

- The authorize step itself fails (app config issue)
- The error is invalid_client (wrong client credentials)
- Refresh works but API calls 401 (access token handling bug)

## Compatibility

Intercom OAuth 2.0, app store integrations, token endpoint auth/eagle/token.

## Variant phrasings

### intercom refresh token expired invalid_grant

Expiry and revocation look identical here. The recovery is the same: fresh grant.

### intercom oauth reauthorize invalid grant

Re-authorization is the only recovery path. There is no refresh of a dead grant.

## Why it happens

Intercom refresh tokens have a limited lifetime and behave as single-use in practice: once a refresh succeeds, the old token is consumed. Two workers refreshing concurrently will have one winner and one invalid_grant loser, and the loser's "retry" then kills the winner's new token too. Add expiry or revocation on top, and invalid_grant is the terminal state. Only a fresh interactive grant mints a new pair.

## Edge cases

- Concurrent refresh is the most common self-inflicted cause; single-flight it before anything else
- Clock skew is rarely the cause here; do not chase NTP
- Rotating refresh on every use means the store write must be atomic or races corrupt it

## If it still fails

- Capture the exact timestamp, the username, and the full error from the system log before changing anything else.
- Reproduce with a single test user so you are not debugging a crowd.
- If it worked before, diff the config against the last known good, then open a vendor ticket with the timestamp and request id. Never send secrets or private keys.

## Prevention

- Track credential and certificate expiry with alerts, not memory.
- Run a synthetic check per app daily so breakage pages you, not a user.
- Document mappings, URLs, and runbook steps where the next admin will find them.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_iEudIIbuJS8p7rKmkE631A
