Octopus Cards
Best Practices

Authentication

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

Token Lifecycle

The Octopus Cards 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 /auth/login once at startup (or on first request), then reuse the access token for its full lifetime. Do not authenticate on every request — this wastes time and increases load on both sides.

// Store the token and its expiry after login
type AuthState struct {
    AccessToken  string
    RefreshToken string
    ExpiresAt    time.Time
}

func (a *AuthState) Token() string {
    // Proactively refresh 5 minutes before expiry
    if time.Until(a.ExpiresAt) < 5*time.Minute {
        a.refresh()
    }
    return a.AccessToken
}

Proactive Refresh

Do not wait for a 401 to refresh. Instead, check access_expires_at from the login response and refresh before the token expires. This avoids failed requests and retry logic.

A good pattern:

  1. Parse access_expires_at from the login/refresh response
  2. Schedule a refresh for ~5 minutes before that time
  3. Swap in the new token atomically
func (a *AuthState) refresh() error {
    body, _ := json.Marshal(map[string]string{
        "refresh_token": a.RefreshToken,
    })

    resp, err := http.Post("{{host}}/auth/refresh", "application/json", bytes.NewReader(body))
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    var result struct {
        Data struct {
            AccessToken      string `json:"access_token"`
            RefreshToken     string `json:"refresh_token"`
            AccessExpiresAt  string `json:"access_expires_at"`
        } `json:"data"`
    }
    json.NewDecoder(resp.Body).Decode(&result)

    a.AccessToken = result.Data.AccessToken
    a.RefreshToken = result.Data.RefreshToken
    a.ExpiresAt, _ = time.Parse(time.RFC3339, result.Data.AccessExpiresAt)

    return nil
}

Token Storage

Never log tokens, commit them to source control, or store them in plain text config files.

Server-side integrations:

  • Store tokens in memory only. They are short-lived by design — persist the credentials (API key + secret), not the tokens.
  • If you must persist tokens across restarts, use an encrypted store (e.g. a secrets manager, encrypted database column, or OS keychain).
  • Never write tokens to disk in plain text, log files, or environment variable dumps.

Environment variables for credentials:

# Store credentials, not tokens
export OCTOPUS_API_KEY="your_api_key"
export OCTOPUS_API_SECRET="your_api_secret"

Refresh Token Security

The refresh token is more sensitive than the access token — it can generate new access tokens for up to 7 days.

  • Store refresh tokens separately from access tokens where possible (e.g. 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 /auth/logout immediately to revoke all tokens for your client.

Concurrency

If your application runs multiple threads or processes making API calls:

  • Share a single token across all threads rather than logging in from each one. Multiple logins create multiple active token sets and waste resources.
  • Synchronize refresh — use a mutex or similar mechanism to ensure only one thread refreshes at a time. Other threads should wait for the new token.
type SafeAuth struct {
    mu    sync.RWMutex
    state AuthState
}

func (s *SafeAuth) Token() string {
    s.mu.RLock()
    if time.Until(s.state.ExpiresAt) > 5*time.Minute {
        defer s.mu.RUnlock()
        return s.state.AccessToken
    }
    s.mu.RUnlock()

    s.mu.Lock()
    defer s.mu.Unlock()

    // Double-check: another goroutine may have refreshed
    if time.Until(s.state.ExpiresAt) > 5*time.Minute {
        return s.state.AccessToken
    }

    s.state.refresh()
    return s.state.AccessToken
}

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)

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
  • Enable IP whitelisting for production
  • Logout on graceful shutdown
  • Never log or expose tokens in error messages

On this page