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.

ConcernWithout cachingWith caching
Latency per API call+200–500ms (token fetch round-trip)~0ms (in-memory or cache lookup)
Auth server load1 token request per API call1 token request per ~2 hours
Rate limit riskHigher, more requests to Snapdocsauth serverNegligible
ReliabilityAdditional point of failure per callToken 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.

📘
Buffer 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:

ApproachBest forTrade-offs
Redis / MemcachedMulti-instance or distributed servicesShared across all instances; requires cache infrastructure
In-memory (singleton)Single-instance apps, serverless with warm startsSimple to implement; lost on restart; not shared across instances
DatabaseWhen neither of the above is availableWorks with any existing database; slightly higher latency per lookup
📘
Use what you have

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.