ETS-backed GenServer that caches Daraja access tokens to eliminate the network round-trip on every API call.
Add Daraja.Supervisor to your application's supervision tree to opt in:
children = [
{Daraja.Supervisor, []}
]The library is process-free by default; caching is opt-in.
Cache key
Tokens are keyed by {consumer_key, environment, secret_hash} where
secret_hash is a SHA-256 digest of consumer_secret. Rotating the secret
while keeping the same consumer key produces a cache miss and fetches a fresh
token. Call invalidate/1 after rotation to evict any orphaned entry keyed
under the previous secret.
TTL and refresh
Token lifetime comes from Safaricom's OAuth expires_in field. :ttl
(default 3600 s) is used only when expires_in is absent or unparseable.
:refresh_before (default 120 s) controls how many seconds before expiry
the cache proactively fetches a fresh token in the background.
refresh_before should be less than the configured :ttl fallback and the
effective token lifetime. When expires_in is shorter than refresh_before,
refresh is scheduled immediately. init/1 raises when refresh_before >= ttl.
Concurrent fetches
OAuth network I/O runs in Task.Supervisor workers — not inside the
GenServer mailbox — so cache misses for different credentials fetch in
parallel. Concurrent misses for the same key share one in-flight task.
Crash and restart
ETS tables are owned by the GenServer process. On crash and supervisor restart, the old table is gone and a fresh one is created — all cached tokens are lost and the next request per client pays a cold network fetch.
ETS access
The cache table is :protected: any process may read (lock-free happy path
in get_token/2), but only the owning GenServer may insert or delete. This
prevents local cache poisoning by unrelated processes while preserving read
concurrency.
Credential retention
Client credentials are held in the GenServer's clients map (keyed like the
token cache) so proactive refresh timers carry only the cache key—not a full
%Daraja.Client{}—in delayed messages.
Summary
Functions
Returns a cached access token for client, fetching from the network on a
miss.
Evicts the cached token for client, canceling any scheduled refresh.
Functions
@spec get_token(Daraja.Client.t(), atom()) :: {:ok, String.t()} | {:error, :auth_failed, term()} | {:error, :http_error, term()}
Returns a cached access token for client, fetching from the network on a
miss.
Reads directly from ETS on the happy path (lock-free). Falls back to a
GenServer.call on a miss or when the ETS table doesn't exist yet (brief
startup race between supervisor start and init/1 completing).
server defaults to Daraja.TokenCache. Pass a custom atom when using a
non-default cache name configured via cache_name: in Daraja.Supervisor.
@spec invalidate(Daraja.Client.t(), atom()) :: :ok
Evicts the cached token for client, canceling any scheduled refresh.
Use after credential rotation to drop a stale entry obtained with the
previous secret. With the updated cache key, a rotated secret already
misses the cache; invalidate/1 cleans up the orphaned entry and timer.
server defaults to Daraja.TokenCache.