Access tokens expire after 1 hour. This guide shows you how to automatically refresh them to keep your integration running smoothly.
Why Do Tokens Expire?
Security. Short-lived access tokens limit damage if they're stolen. Even if someone intercepts your access token, it only works for 1 hour.
OAuth 2.0 encourages short-lived access tokens and long-lived refresh tokens, and leaves the lifetimes to each implementation. The reasons:
- Token compromise: a short-lived access token limits how long an attacker can use a stolen one.
- Least privilege: access is granted for the shortest time it is needed.
- Rotation: short lifetimes mean tokens are replaced often, which makes a stolen one harder to misuse.
- Audit and revoke: short-lived tokens are easier to audit, and to revoke one at a time.
- Refresh tokens: a new access token is obtained with a refresh token, and only the app the refresh token was issued to can use it: the refresh request carries that app's client credentials.
The OAuth 2.0 Security Best Current Practice and oauth.com's guide to refreshing an access token cover the background.
Two types of tokens:
| Token Type | Lifespan | Purpose |
|---|---|---|
| Access Token | 1 hour | Make API calls |
| Refresh Token | No fixed expiry; works once | Get new access tokens |
How Long a Refresh Token Lasts
A refresh token has no fixed expiry. It works once: each refresh returns a new access token and a new refresh token, and revokes the old pair straight away.
A refresh token stops working when:
- it has been used to refresh, even if your app never received the response;
-
your app revokes it, or the access token it was issued with, at
POST /oauth/revoke; - the user removes your app's access in their DoorFlow settings;
- DoorFlow support revokes all of the user's tokens;
- the user, or your OAuth application, is deleted.
Any of these makes the next refresh fail with invalid_grant, and the user has to authorize your app again.
Think of it like a hotel key card:
- Access token = Your room key (expires when you check out)
- Refresh token = Your ID at the front desk (get a new room key anytime)
What Happens When Tokens Expire
Authorization: Bearer [access_token] Note over API: Token expired! API-->>App: 401 Unauthorized
"The access token expired" Note over App: Need to refresh the token
When your access token expires, the API returns 401 Unauthorized. This is your signal to refresh.
The Token Refresh Flow
Here's the complete process when a token expires:
Authorization: Bearer [expired_token] API-->>App: 401 Unauthorized Note over App: 2. Get refresh token from storage App->>DB: Get refresh_token DB-->>App: refresh_token Note over App: 3. Request new tokens App->>API: POST /oauth/token
grant_type=refresh_token
refresh_token=...
client_id=...
client_secret=... Note over API: Validates refresh token API-->>App: 200 OK
New access_token
New refresh_token Note over App: 4. Store BOTH new tokens App->>DB: Store new access_token
Store new refresh_token Note over App: 5. Retry original request App->>API: GET /api/3/people
Authorization: Bearer [new_token] API-->>App: 200 OK
People data
How to Refresh a Token
Request:
curl -X POST "https://api.doorflow.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=YOUR_REFRESH_TOKEN" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"
Parameters
grant_type - Always "refresh_token"
refresh_token - Your current refresh token (from storage)
client_id - Your OAuth app's client ID
client_secret - Your OAuth app's client secret
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def502003d8f9a7b2c1e4d5f6g7h8i9j0k...",
"scope": "account.person",
"created_at": 1704070800
}
Critical: You receive BOTH:
- New access token - Use this for API calls
- New refresh token - Store this and use it for the next refresh
The old refresh token is now invalid. Always use the newest refresh token.
Implementation Approach 1: Reactive Refresh
Strategy: Wait for 401, then refresh and retry.
with access_token] Call --> Check{Response
status?} Check -->|200 OK| Success([Return data]) Check -->|401 Unauthorized| Refresh[Use refresh_token to get
new access_token] Check -->|Other error| Error([Handle error]) Refresh --> Store[Store new access_token
and new refresh_token] Store --> Retry[Retry original API request
with new access_token] Retry --> Success
When to use
Tradeoff
Implementation Approach 2: Proactive Refresh
Strategy: Check expiration time before making calls. Refresh early if needed.
and expires_at from storage] Load --> Check{Token expires
in next
5 minutes?} Check -->|Yes| Refresh[Use refresh_token to get
new access_token] Check -->|No| UseExisting[Use existing access_token] Refresh --> Store[Store new access_token
and new refresh_token] Store --> Call[Make API request
with new access_token] UseExisting --> Call Call --> Success([Return data])
When to use
Tradeoff
How to track expiration:
When you store tokens, also calculate and store the expiration time:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "def502003d8f9a7b2c1e4d5f6g7h8i9j0k...",
"expires_at": 1704074400
}
Calculation
expires_in: 3600 (seconds)
expires_at = current_time + 3600
current_time + 300 (5 min buffer) > expires_at
When Token Refresh Fails
Not all refresh attempts succeed. Here's how to handle failures:
status?} Check -->|200 OK| Success[Store new tokens] Check -->|400 Bad Request| ParseError{Error type?} Success --> Done([Continue with API calls]) ParseError -->|invalid_grant| InvalidToken[Refresh token already
used or revoked] ParseError -->|invalid_client| WrongCreds[Client ID/secret
is incorrect] ParseError -->|unauthorized_client| NotAuth[OAuth app settings
problem] InvalidToken --> Reauth[Clear stored tokens
Redirect user to
OAuth authorization flow] WrongCreds --> FixCreds([Check your
client credentials]) NotAuth --> CheckSettings([Check OAuth app
configuration]) Reauth --> End([User must reconnect])
Common errors:
| Error | Meaning | Solution |
|---|---|---|
invalid_grant |
Refresh token already used, or revoked | User must re-authorize your app |
invalid_client |
Client ID or secret wrong | Check your OAuth app credentials |
unauthorized_client |
OAuth app not authorized for this grant type | Check OAuth app settings in DoorFlow |
Most common: invalid_grant means the refresh token was already used, or was revoked (see How Long a Refresh Token Lasts). Retrying cannot help. Clear stored tokens and send the user through OAuth authorization again.
Things to Watch Out For
Because a refresh token works once, how your app handles a refresh decides whether it keeps its access:
- Replace tokens together. When a refresh succeeds, store the new access token and the new refresh token in one atomic write. The old pair no longer works.
-
Refresh one at a time. Two refreshes with the same refresh token cannot both succeed: DoorFlow answers the second with
invalid_grant. Make sure only one request per user refreshes at once, and let the others wait for its result. - Handle errors. Check that a refresh response is what you expect before you use it, and log and alert on failures so someone can step in.
-
Set timeouts. Give the refresh request a timeout so a hung request does not block your app. If a refresh times out, the old refresh token may already be revoked: the next attempt then fails with
invalid_grant. - Validate new tokens before use. Check the new tokens' format and values before they replace the old ones.
-
Retry with exponential backoff, but not on
invalid_grant. Retry a refresh that failed on a network error or a 5xx response, backing off each time so you do not overwhelm the token endpoint. Aninvalid_grantis final. - Log and monitor token operations, never token values. Log each refresh and its outcome, and alert on errors. Never write an access or refresh token to a log.
- Have a manual fallback. Give support a way to reconnect a user by hand when automated refresh fails, as a last resort.
Storing Tokens Securely
Security requirements
What to store:
{
"user_id": 12345,
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "ENCRYPTED:def502003d8f9a7b2c1e4d5f6g7h8i9j0k...",
"expires_at": 1704074400,
"scope": "account.person account.channel.readonly"
}
Critical: Refresh tokens are long-lived and powerful. If someone steals a refresh token, they can access the API indefinitely. Always encrypt them.
Testing Your Implementation
How to test token refresh
Manually expire a token in your database:
- Set
expires_atto a time in the past - Example:
expires_at = current_time - 1000
expires_at to a time in the past
expires_at = current_time - 1000
Make an API call - Should trigger your refresh logic
Verify:
- API call succeeds
- New tokens stored in database
- New
expires_atis ~1 hour in the future
expires_at is ~1 hour in the future
Test error handling
Test invalid refresh token:
- Change the refresh token in your database to
"invalid_token" - Make an API call
- Verify user is sent through OAuth flow again
"invalid_token"
Test network errors:
- Temporarily break network connection
- Make an API call
- Verify appropriate error message shown to user
Complete Flow Example
Here's everything together - from initial OAuth through token refresh:
(exchange code) API-->>App: access_token + refresh_token App->>DB: Store both tokens + expires_at Note over User,DB: Making API Calls (token valid) App->>DB: Get access_token DB-->>App: access_token App->>API: GET /api/3/people
Bearer [access_token] API-->>App: 200 OK (people data) Note over User,DB: 1 Hour Later (token expired) App->>DB: Get access_token DB-->>App: access_token (expired) App->>API: GET /api/3/people
Bearer [expired_token] API-->>App: 401 Unauthorized Note over App: Token expired - refresh it App->>DB: Get refresh_token DB-->>App: refresh_token App->>API: POST /oauth/token
grant_type=refresh_token API-->>App: New access_token + refresh_token App->>DB: Store new tokens App->>API: GET /api/3/people
Bearer [new_token] API-->>App: 200 OK (people data)
Quick Decision Guide
Which refresh strategy should I use?
integration?} Start -->|Background job
runs every few hours| Reactive[Use Reactive Refresh
Wait for 401, then refresh] Start -->|User-facing web app
Real-time updates| Proactive[Use Proactive Refresh
Check expiration before calls] Start -->|Mobile app| Proactive Start -->|Webhook handler
Low traffic| Reactive Reactive --> Simple[Simpler to implement
Occasional 401 is fine] Proactive --> Complex[More complex
No failed requests]
When do I need to re-authorize the user?
Only when refresh token is invalid:
- User revoked your app's access
- OAuth app credentials changed
- OAuth app was deleted
- Refresh token manually deleted
Regular expiration of access tokens does NOT require re-authorization.
Common Mistakes
1. Not storing the new refresh token
Each refresh gives you a NEW refresh token. The old one is invalid.
Wrong
Right
2. Not handling refresh failures
Refresh can fail. Always have a fallback to re-authorize.
3. Logging tokens
Never log access or refresh tokens. Use [REDACTED] in logs.
4. Storing tokens in localStorage (web apps)
Use secure, httpOnly cookies or server-side sessions. localStorage is vulnerable to XSS attacks.
5. Sharing tokens between users
Each user needs their own tokens. Never reuse tokens across different users.
Quick Reference
Refresh a token:
POST https://api.doorflow.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
refresh_token=YOUR_REFRESH_TOKEN
client_id=YOUR_CLIENT_ID
client_secret=YOUR_CLIENT_SECRET
Response
access_token - Use for API calls (1 hour lifetime)
refresh_token - Store for next refresh (replaces old one)
expires_in - 3600 (seconds)
When to refresh
What to store
access_token
refresh_token (encrypted)
expires_at (current time + expires_in)
scope
Security rules
Next Steps
Now that you understand token refresh:
- [OAuth Authorization Flow] - See where refresh tokens come from
- [Choosing the Right Scopes] - Select appropriate permissions
- [Error Handling] - Handle all API error scenarios
- [Common Workflows] - See complete integration examples