stemp Logostemp Developer

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
}
FieldDescription
codeMachine-readable error code (lowercase, snake_case)
paramsOptional 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"
}
ErrorDescription
invalid_requestMissing or invalid parameters
invalid_clientInvalid client ID or secret
invalid_grantAuthorization code expired or already used
invalid_scopeRequested scope is not allowed for this app
unauthorized_clientClient 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:action format.

Troubleshooting Checklist

SymptomCheck
All requests return 401Is your token expired? Try refreshing it.
Specific endpoint returns 403Does your token have the required scope?
Create user returns 409A user with this email already exists in your organization.
Webhook not receivedIs your endpoint HTTPS? Does it return 200 quickly?
Pass creation failsDoes the template exist? Is it active?
Stamps not updatingDoes the template have the stamp feature enabled?