Article
Authentication Mastery with Smartsheet API
Link copied
Mastering Smartsheet API Series — Part 3
This post is part of the Mastering Smartsheet API series, a practical companion to the Smartsheet API documentation.
New learning blogs drop regularly — join the API & Developers group in the Smartsheet Community and click Follow → Include in Email Digest to get each new blogpost delivered straight to your inbox via the weekly digest.
You've got tokens working. Your local scripts authenticate without issues. Then you move to production and everything starts breaking at 2 AM — a refresh token silently expires, a service account loses sheet access after a permission change, or a 401 you haven't seen before starts flooding your logs.
Authentication in Smartsheet isn't complicated. But the edge cases are real, and they will surface in production. This post covers both auth methods, the error patterns that actually matter, and the code patterns that handle them correctly.
Two auth methods, two use cases
Personal Access Tokens (PATs) are the right choice for internal tools, scripts, and automation that runs on behalf of a single user. They don't expire unless revoked, they're simple to rotate, and they authenticate the user who created them — meaning the token inherits that user's sheet permissions exactly.
PATs have three practical advantages in production: there's no token exchange overhead, so latency is minimal; permissions are fully predictable — the token can do exactly what the user can do, nothing more; and audit is straightforward — you always know which PAT triggered which action.
OAuth 2.0 is the right choice for multi-user integrations, SaaS products, or anything where you're acting on behalf of other people's Smartsheet accounts. It's more complex to implement, but it's the only appropriate model when your application serves more than one user.
OAuth gives you three things PATs can't: you act on behalf of the user, not yourself — which means their permissions govern what the integration can touch, not yours; scopes let you request least-privilege access — if your integration only needs to read sheets, you request read-only and that's all the token can do; and users can revoke access themselves at any time without involving you.
If you're building something internal, PATs are almost always the better choice. If you're building a product that connects to customers' Smartsheet accounts, OAuth is mandatory.
PAT authentication
PATs authenticate via the Authorization header:
Authorization: Bearer YOUR_TOKEN
In Python:
PYTHON
client = smartsheet.Smartsheet("YOUR_TOKEN")
client.errors_as_exceptions(True)
errors_as_exceptions(True) converts API errors into Python exceptions rather than returning error objects silently — recommended for production code.
Rotation: PATs don't expire, but you should rotate them when team members leave, when a token is exposed, or on a regular schedule for compliance purposes. Treat a PAT like a password: store it in environment variables or a secrets manager, never in source code.
OAuth 2.0
OAuth 2.0 in Smartsheet follows the standard authorization code flow:
- Your application redirects the user to the Smartsheet authorization URL
- The user approves the requested scopes
- Smartsheet redirects back to your callback URL with an authorization code
- You exchange that code for an access token and refresh token
- You use the access token for API calls; when it expires, you use the refresh token to get a new one
Token lifetimes
Authorization code: valid for approximately 10 minutes after it's issued. Exchange it immediately — don't store it, don't pass it around your application.
Access token: valid for 7 days.
Refresh token: expires after a set period that depends on your Smartsheet plan configuration. This is the one that will break your integration if you're not tracking it correctly.
Exchanging the authorization code
Once Smartsheet redirects back to your callback URL with a code parameter, you have roughly 10 minutes to exchange it. Here's what that looks like in practice:
PYTHON
import requests
def exchange_authorization_code(code):
"""
Exchange a short-lived authorization code for an access token + refresh token.
Call this immediately when the code arrives at your callback endpoint.
"""
response = requests.post(
"https://api.smartsheet.com/2.0/token",
data={
"grant_type": "authorization_code",
"client_id": os.environ["SMARTSHEET_CLIENT_ID"],
"client_secret": os.environ["SMARTSHEET_CLIENT_SECRET"],
"code": code,
"redirect_uri": os.environ["SMARTSHEET_REDIRECT_URI"],
},
)
response.raise_for_status()
data = response.json()
# Persist both tokens immediately — you'll need the refresh token later
return save_tokens(
access_token=data["access_token"],
refresh_token=data["refresh_token"],
)
The response includes both the access token and the refresh token. Persist both immediately — the refresh token is what keeps your integration alive beyond the first 7 days.
The refresh token window
Refresh tokens expire after a period set at the plan level — check your Smartsheet plan configuration for the exact duration. The window resets every time you use the refresh token successfully. In practice: if your integration refreshes the access token regularly, the refresh token stays valid. If it doesn't — for example, if a user doesn't use your integration for an extended period — the refresh token expires and the user has to go through the full OAuth flow again.
This is the most common silent failure in OAuth integrations. The fix is explicit tracking:
PYTHON
import json
import os
TOKEN_FILE = "tokens.json"
REFRESH_TOKEN_EXPIRY_DAYS = 28 # verify against your Smartsheet plan configuration
WARNING_THRESHOLD_DAYS = 7 # warn when fewer than 7 days remain
def load_tokens():
if os.path.exists(TOKEN_FILE):
with open(TOKEN_FILE, "r") as f:
return json.load(f)
return None
def save_tokens(access_token, refresh_token, refresh_token_issued_at=None):
tokens = {
"access_token": access_token,
"refresh_token": refresh_token,
"refresh_token_issued_at": refresh_token_issued_at or time.time(),
}
with open(TOKEN_FILE, "w") as f:
json.dump(tokens, f)
return tokens
def check_refresh_token_health(tokens):
issued_at = tokens.get("refresh_token_issued_at", 0)
elapsed_days = (time.time() - issued_at) / 86400
remaining_days = REFRESH_TOKEN_EXPIRY_DAYS - elapsed_days
if remaining_days <= 0:
raise RuntimeError(
"Refresh token has expired. User must re-authenticate via OAuth."
)
elif remaining_days <= WARNING_THRESHOLD_DAYS:
print(
f"WARNING: Refresh token expires in {remaining_days:.1f} days. "
"Trigger re-authentication before it expires."
)
return remaining_days
def refresh_access_token(refresh_token):
response = requests.post(
"https://api.smartsheet.com/2.0/token",
data={
"grant_type": "refresh_token",
"client_id": os.environ["SMARTSHEET_CLIENT_ID"],
"client_secret": os.environ["SMARTSHEET_CLIENT_SECRET"],
"refresh_token": refresh_token,
},
)
response.raise_for_status()
data = response.json()
# Save new tokens and reset the 28-day window
return save_tokens(
access_token=data["access_token"],
refresh_token=data["refresh_token"],
refresh_token_issued_at=time.time(),
)
In production, store token state in a database rather than a file — especially if your integration serves multiple users. The principle is the same: track refresh_token_issued_at explicitly and alert before it expires.
Error handling
Smartsheet uses HTTP status codes alongside API-specific error codes in the response body. The combination tells you what actually happened.
401 errors
A 401 means the request wasn't authenticated. The API error code tells you why:
PYTHON
classifications = {
1001: "PAT or OAuth access token is missing or malformed.",
1002: "OAuth access token has expired. Refresh it.",
1003: "PAT or OAuth access token is invalid or has been revoked.",
1004: "OAuth access token does not have the required scope for this operation.",
}
return classifications.get(error_code, f"Unknown 401 error code: {error_code}")
The practical distinction that matters:
- 1002 (expired): refresh the access token and retry — this is recoverable automatically
- 1003 (invalid/revoked): the token itself is bad; refresh won't help — the user needs to re-authenticate
- 1004 (insufficient scope): the token is valid but lacks permission for this operation — check the scopes you requested during OAuth setup
403 errors
A 403 means authentication succeeded but authorization failed — the token is valid, but the authenticated user doesn't have permission to perform the action on that resource.
This is the most common source of confusion with service accounts. A service account can have a valid PAT and still get 403s on sheets it hasn't been given explicit access to. The token being valid doesn't mean the account has been added as a collaborator on the specific sheet.
PYTHON
classifications = {
1006: "User does not have permission to perform this action.",
4003: "User is not a collaborator on this sheet. Add the service account to the sheet.",
4004: "Admin permissions required for this operation.",
}
return classifications.get(error_code, f"Unknown 403 error code: {error_code}")
Service accounts in production
For server-side automation, use a dedicated service account with a PAT rather than a personal user account. This keeps automation independent of individual employment status and makes permission management explicit.
The critical thing to understand: a service account is a Smartsheet user like any other. It needs to be added as a collaborator to any sheet it needs to access. A valid PAT and a 403 on a specific sheet usually means one thing: the service account hasn't been given access to that sheet.
A diagnostic function for this:
PYTHON
def diagnose_service_account_access(token, sheet_id):
client = smartsheet.Smartsheet(token)
try:
sheet = client.Sheets.get_sheet(sheet_id)
print(f"Access confirmed: '{sheet.name}' (ID: {sheet_id})")
return True
except smartsheet.exceptions.ApiError as e:
error = e.error.result
print(f"Error {error.error_code}: {error.message}")
if error.error_code == 4003:
print(
"Service account is not a collaborator on this sheet. "
"Add it via the sheet's sharing settings."
)
elif error.error_code == 1003:
print("Token is invalid or revoked. Generate a new PAT.")
elif error.error_code == 1002:
print("Access token has expired. Refresh it.")
return False
The production authentication checklist
Before you go to production:
PAT integrations:
- Token stored in environment variable or secrets manager, not in code
- Rotation plan documented
- Error handling covers 401 (1001, 1002, 1003, 1004) and 403 (1006, 4003, 4004)
OAuth integrations:
- Authorization code exchanged immediately at the callback endpoint
refresh_token_issued_attracked in persistent storage- Re-authentication flow tested — what happens when the refresh token expires?
- Warning threshold set before the 28-day window closes
- Scopes reviewed — request only what your integration actually needs
Service accounts:
- Dedicated account created (not a personal user)
- Account added as collaborator to all required sheets
- Permissions level verified (Viewer vs. Editor vs. Admin) per sheet
What's next
The authentication layer is stable when your tokens are valid and your service account has been provisioned correctly. The next layer of production hardening is what happens when the API itself pushes back — rate limits, transient failures, and the retry strategies that keep your integration running under load. That's Part 4.
This post is part of the Mastering Smartsheet API series, designed as a practical companion to the Smartsheet API documentation. For questions, visit the Smartsheet Community or the developer forum.
Link copied