Cache and reuse bearer tokens
Snapdocs APIs use OAuth 2.0 with the client_credentials grant. When you request a token, the response includes an expires_in field indicating how long the token remains valid (typically 2 hours / 7200 seconds). Cache the token and reuse it for its full lifetime; your application then avoids the overhead of fetching a new token on every API call. Token caching is a client-side change only; nothing needs to change on the Snapdocs side.
| Concern | Without caching | With caching |
|---|---|---|
| Latency per API call | +200–500ms (token fetch round-trip) | ~0ms (in-memory or cache lookup) |
| Auth server load | 1 token request per API call | 1 token request per ~2 hours |
| Rate limit risk | Higher, more requests to Snapdocsauth server | Negligible |
| Reliability | Additional point of failure per call | Token fetch failure is isolated |
How token caching works
Your application checks for a cached token before each API call and only requests a new one when the cache is empty or the token has expired.
sequenceDiagram participant Client as Your Application participant Cache as Token Cache (Redis/Memory) participant Auth as Auth0 (Token Endpoint) participant API as Snapdocs API Client->>Cache: Check for cached token Cache-->>Client: Cache MISS Client->>Auth: POST /oauth/token (client_credentials) Auth-->>Client: { access_token, expires_in: 7200 } Client->>Cache: Store token (TTL = expires_in - 60s buffer) Client->>API: API request with Bearer token API-->>Client: 200 OK Note over Client,Cache: Subsequent API call (within token lifetime) Client->>Cache: Check for cached token Cache-->>Client: Cache HIT ✅ Client->>API: API request with Bearer token API-->>Client: 200 OK Note over Client,Auth: Token fetched only when cache is empty or expired
When to fetch vs. reuse
flowchart TD A[API Call Needed] --> B{Cached token exists?} B -- No --> C[Fetch new token from Auth0] C --> D[Cache token with TTL = expires_in - 60s] D --> E[Make API call with Bearer token] B -- Yes --> F{Token expired or near expiry?} F -- No --> E F -- Yes --> C E --> G{Response 401 Unauthorized?} G -- No --> H[Process response] G -- Yes --> I[Invalidate cached token] I --> C
Implementation
Step 1: store the token with expiration metadata
When you receive a token response, store both the access_token and its calculated expiration time.
Subtract a buffer (e.g. 60 seconds) from the expires_in value when computing the TTL. Your application then refreshes the token before it actually expires, avoiding failed calls due to clock skew or in-flight request timing.
A typical token response:
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 7200 }
Step 2: check the cache before every API call
Only fetch a new token if the cache is empty or the token has expired. Use a mutex or lock so concurrent requests don't each fetch their own token.
Step 3: handle 401 responses
If the API returns 401 Unauthorized, the token may have been revoked or is otherwise invalid. Invalidate your cached token, fetch a new one, and retry the request once.
Code examples
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
public class SnapdocsTokenManager
{
private const int BufferSeconds = 60;
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _audience;
private readonly string _tokenUrl;
private string _accessToken;
private DateTime _expiresAt = DateTime.MinValue;
private readonly SemaphoreSlim _semaphore = new(1, 1);
private static readonly HttpClient _httpClient = new();
public SnapdocsTokenManager(string clientId, string clientSecret,
string audience, string tokenUrl)
{
_clientId = clientId;
_clientSecret = clientSecret;
_audience = audience;
_tokenUrl = tokenUrl;
}
public async Task<string> GetTokenAsync()
{
if (IsTokenValid()) return _accessToken;
await _semaphore.WaitAsync();
try
{
if (IsTokenValid()) return _accessToken;
await FetchNewTokenAsync();
return _accessToken;
}
finally
{
_semaphore.Release();
}
}
public void Invalidate()
{
_accessToken = null;
_expiresAt = DateTime.MinValue;
}
private bool IsTokenValid() =>
_accessToken != null && DateTime.UtcNow < _expiresAt;
private async Task FetchNewTokenAsync()
{
var payload = new
{
client_id = _clientId,
client_secret = _clientSecret,
audience = _audience,
grant_type = "client_credentials"
};
var content = new StringContent(
JsonSerializer.Serialize(payload),
Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(_tokenUrl, content);
response.EnsureSuccessStatusCode();
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
_accessToken = json.RootElement.GetProperty("access_token").GetString();
var expiresIn = json.RootElement.TryGetProperty("expires_in", out var exp)
? exp.GetInt32() : 7200;
_expiresAt = DateTime.UtcNow.AddSeconds(expiresIn - BufferSeconds);
}
}Choosing a cache backend
Any of these approaches works. Choose the one that fits your existing infrastructure:
| Approach | Best for | Trade-offs |
|---|---|---|
| Redis / Memcached | Multi-instance or distributed services | Shared across all instances; requires cache infrastructure |
| In-memory (singleton) | Single-instance apps, serverless with warm starts | Simple to implement; lost on restart; not shared across instances |
| Database | When neither of the above is available | Works with any existing database; slightly higher latency per lookup |
If your application already runs Redis or Memcached, use it: the token is shared across all instances and survives restarts. Otherwise an in-memory singleton is the simplest option for single-instance apps, and a database table with an expiration timestamp is a valid fallback. A database read of a small single row table costs far less than a API call for a token fetch.