Authentication
Best practices for token generation, storage, rotation, and security
Token lifecycle
The API issues two tokens on login:
| Token | Lifetime | Purpose |
|---|---|---|
| Access token | 1 hour | Authenticates every API request |
| Refresh token | 7 days | Obtains a new token pair without re-entering credentials |
Design your integration around this lifecycle. Never treat tokens as long-lived secrets.
Generate tokens once, reuse until expiry
Call POST /auth/login once at startup (or on first request), then reuse the access token for its full lifetime. Do not authenticate on every request — that wastes time, multiplies active token sets, and increases load on both sides.
Wrap the token state in a small client object, and route every API call through its token() accessor. The accessor returns the current token if it still has a comfortable margin (5 minutes), and refreshes otherwise.
type AuthClient struct {
host string
username string
password string
accessToken string
refreshToken string
expiresAt time.Time
}
func (a *AuthClient) Token(ctx context.Context) (string, error) {
if time.Until(a.expiresAt) < 5*time.Minute {
if err := a.refresh(ctx); err != nil {
return "", err
}
}
return a.accessToken, nil
}Proactive refresh
Do not wait for a 401 to refresh — parse access_expires_at from the login/refresh response and refresh before the access token expires. This avoids failed requests and retry logic in the hot path.
A good pattern:
- Parse
access_expires_atfrom the login/refresh response - Treat anything within 5 minutes of expiry as "needs refresh"
- Swap in the new tokens atomically once
/auth/refreshreturns
func (a *AuthClient) refresh(ctx context.Context) error {
payload, _ := json.Marshal(map[string]string{
"refresh_token": a.refreshToken,
})
req, _ := http.NewRequestWithContext(ctx, "POST",
a.host+"/auth/refresh", bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer func() { _ = resp.Body.Close() }()
var out struct {
Data struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
AccessExpiresAt string `json:"access_expires_at"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return err
}
a.accessToken = out.Data.AccessToken
a.refreshToken = out.Data.RefreshToken
a.expiresAt, _ = time.Parse(time.RFC3339, out.Data.AccessExpiresAt)
return nil
}If the refresh call itself fails with 401 (refresh token expired or revoked), fall back to a fresh POST /auth/login with credentials.
Token storage
Never log tokens, commit them to source control, or store them in plain text config files.
- Store tokens in memory only. They are short-lived by design — persist the credentials (API key + secret), not the tokens themselves.
- If you must persist tokens across restarts, use an encrypted store: a secrets manager, an encrypted database column, or the OS keychain.
- Never write tokens to disk in plain text, log files, or environment variable dumps.
Credentials in environment (not tokens):
export API_KEY="your_api_key"
export API_SECRET="your_api_secret"Refresh token security
The refresh token is more sensitive than the access token — it can mint new access tokens for up to 7 days.
- Store refresh tokens separately from access tokens where possible (different memory scope, different encryption key).
- Never send the refresh token in API request headers. It is only used in the body of
POST /auth/refresh. - Rotate on every refresh — when you call
/auth/refresh, the old refresh token is automatically revoked and a new one is issued. Always store the new one. - If a refresh token is compromised, call
POST /auth/logoutimmediately to revoke all tokens for your client.
Concurrency
If your application runs multiple threads, goroutines, or async tasks making API calls simultaneously:
- Share a single token across all callers rather than logging in from each one. Multiple logins create multiple active token sets and waste resources.
- Synchronize refresh — use a mutex / lock so that only one caller refreshes at a time. Other callers wait and then read the new token.
type SafeAuthClient struct {
AuthClient
mu sync.Mutex
}
func (s *SafeAuthClient) Token(ctx context.Context) (string, error) {
// Fast path: still well within validity
s.mu.Lock()
defer s.mu.Unlock()
if time.Until(s.expiresAt) > 5*time.Minute {
return s.accessToken, nil
}
if err := s.refresh(ctx); err != nil {
return "", err
}
return s.accessToken, nil
}IP whitelisting
For production integrations, configure IP whitelisting in your dashboard. This restricts API access to specific CIDR ranges and prevents stolen tokens from being used outside your infrastructure.
- Whitelist the egress IPs of your servers, not broad ranges.
- If you use a cloud provider, whitelist the NAT gateway or static IP range.
- Test your whitelist in sandbox first — the same restriction applies to login, refresh, and all protected endpoints.
Logout on shutdown
When your application shuts down gracefully, call POST /auth/logout to revoke all active tokens. This is especially important for:
- Deployment rollouts (old instances should clean up)
- CI/CD test runs (avoid token accumulation)
- Credential rotation (logout before switching to new credentials)
func (a *AuthClient) Logout(ctx context.Context) error {
req, _ := http.NewRequestWithContext(ctx, "POST",
a.host+"/auth/logout", nil)
req.Header.Set("Authorization", "Bearer "+a.accessToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
_ = resp.Body.Close()
a.accessToken, a.refreshToken = "", ""
return nil
}
// Wire it into shutdown:
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
<-sig
_ = auth.Logout(context.Background())Checklist
- Login once at startup, reuse the access token
- Refresh proactively before expiry (not after a
401) - Store tokens in memory, credentials in a secrets manager
- Synchronize token refresh across threads / async callers
- Enable IP whitelisting for production
- Logout on graceful shutdown
- Never log or expose tokens in error messages