GuidesBest Practices

Authentication

Best practices for token generation, storage, rotation, and security

Token lifecycle

The API issues two tokens on login:

TokenLifetimePurpose
Access token1 hourAuthenticates every API request
Refresh token7 daysObtains 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:

  1. Parse access_expires_at from the login/refresh response
  2. Treat anything within 5 minutes of expiry as "needs refresh"
  3. Swap in the new tokens atomically once /auth/refresh returns
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/logout immediately 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

On this page