Error Handling
Handle API errors gracefully with this troubleshooting guide.
Error Handling
This guide covers common API errors, their causes, and how to handle them in your integration.
Error Response Formats
The stemp API uses two error response formats depending on the type of error.
General Errors
Most errors return a JSON object with a code and optional params:
{
"code": "resource_not_found",
"params": null
}| Field | Description |
|---|---|
code | Machine-readable error code (lowercase, snake_case) |
params | Optional array of contextual parameters, or null |
Validation Errors
Validation failures return a map of field names to error messages under a STANDARD_VALIDATION key:
{
"STANDARD_VALIDATION": {
"email": "must not be blank",
"name": "must be at most 150 characters"
}
}Custom validation errors use a CUSTOM_VALIDATION key with error codes mapped to parameter arrays:
{
"CUSTOM_VALIDATION": {
"email_already_exists": ["jane@example.com"]
}
}HTTP Status Codes
Client Errors (4xx)
400 Bad Request
The request is malformed or missing required fields.
{
"code": "wrong_param_type",
"params": ["userId"]
}Fix: Check that your request body is valid JSON with the correct Content-Type: application/json header.
401 Unauthorized
The access token is missing, expired, or invalid.
Fix: Refresh your access token using the refresh flow, or obtain a new token.
403 Forbidden
The token is valid but lacks the required scope.
{
"code": "access_denied",
"message": "Insufficient scope: requires pass:create"
}Fix: Request the missing scope in your OAuth authorization request or add it to your API token.
404 Not Found
The requested resource doesn't exist.
{
"code": "resource_not_found",
"params": null
}Fix: Verify the resource ID. The resource may have been deleted or may belong to a different organization.
409 Conflict
A resource conflict, such as trying to create a duplicate.
{
"code": "invitation_already_exists",
"params": null
}Fix: Use a different identifier, or fetch the existing resource instead.
422 Unprocessable Entity
Validation failed on one or more fields.
{
"STANDARD_VALIDATION": {
"email": "must be a valid email address",
"name": "must not be blank"
}
}Fix: Check each field in the response and correct the corresponding values in your request.
429 Too Many Requests
You've hit the rate limit.
Fix: Wait for the duration specified in the Retry-After header, then retry. See Rate Limiting.
Server Errors (5xx)
500 Internal Server Error
An unexpected error occurred on stemp's servers.
Fix: Retry after a short delay. If the error persists, contact support.
OAuth2 Errors
OAuth endpoints return errors in a different format per the OAuth2 spec:
{
"error": "invalid_grant",
"error_description": "Authorization code is expired or already used"
}| Error | Description |
|---|---|
invalid_request | Missing or invalid parameters |
invalid_client | Invalid client ID or secret |
invalid_grant | Authorization code expired or already used |
invalid_scope | Requested scope is not allowed for this app |
unauthorized_client | Client not authorized for this grant type |
Best Practices
Retry Strategy
Use exponential backoff for retryable errors:
async function apiCall(fn: () => Promise<Response>, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fn()
if (response.ok) return response
// Don't retry client errors (except 429)
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
throw new ApiError(await response.json())
}
// Respect Retry-After header
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '5')
await sleep(retryAfter * 1000)
continue
}
// Exponential backoff for 5xx errors
if (attempt < maxRetries) {
await sleep(Math.pow(2, attempt) * 1000)
}
}
throw new Error('Max retries exceeded')
}Token Refresh on 401
Automatically refresh tokens when you receive a 401:
async function authenticatedFetch(url: string, options: RequestInit) {
let response = await fetch(url, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${accessToken}` },
})
if (response.status === 401) {
accessToken = await refreshAccessToken()
response = await fetch(url, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${accessToken}` },
})
}
return response
}Validate Before Sending
Catch errors early by validating on your side:
- Email format — Validate before calling
POST /v1/users. - Required fields — Check for blank strings and null values.
- String length — Respect max lengths (email: 254, name: 150, phone: 20).
- Scope format — Ensure scope strings match the
category:actionformat.
Troubleshooting Checklist
| Symptom | Check |
|---|---|
| All requests return 401 | Is your token expired? Try refreshing it. |
| Specific endpoint returns 403 | Does your token have the required scope? |
| Create user returns 409 | A user with this email already exists in your organization. |
| Webhook not received | Is your endpoint HTTPS? Does it return 200 quickly? |
| Pass creation fails | Does the template exist? Is it active? |
| Stamps not updating | Does the template have the stamp feature enabled? |