Skip to content

API Error Responses

This guide provides comprehensive information about error responses from the Grizzly API, including HTTP status codes, error formats, and common error scenarios.

Error Response Format

All API error responses follow a consistent JSON format:

{
  "message": "Description of what went wrong"
}

Key Points:

  • Errors always include a message field describing the error
  • The HTTP status code indicates the error category
  • Error messages are designed to be actionable and developer-friendly

HTTP Status Codes

The Grizzly API uses standard HTTP status codes to indicate success or failure.

2xx Success

Code Status Description
200 OK Request succeeded
201 Created Resource successfully created

4xx Client Errors

Code Status Description Common Causes
400 Bad Request Invalid request format or parameters Missing required fields, invalid JSON, malformed data
401 Unauthorized Authentication failed Invalid or missing API key, expired token
403 Forbidden Insufficient permissions or quota exceeded Missing required permission flags, resource limit reached
404 Not Found Resource doesn't exist KeyRing not found, User doesn't exist, invalid ID
409 Conflict The request conflicts with the resource's current state An Asset parent update would create a relationship cycle
422 Unprocessable Entity Request is valid but contains semantic errors Asset validation failed

5xx Server Errors

Code Status Description
500 Internal Server Error Unexpected server error

Common Error Scenarios

Authentication Errors (401)

Invalid API Key

Request:

curl -X GET https://your-tenant.grizzlycbg.io/ks/keyrings \
  -H "Authorization: Bearer invalid-api-key"

Response:

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "message": "Unauthorized"
}

Common Causes:

  • API key is incorrect or doesn't exist
  • API key has been disabled
  • API key belongs to a different tenant
  • Missing Bearer prefix in Authorization header

Solutions:

  • Verify API key is correct and active
  • Check that API key is properly formatted: Bearer apikey-xxx
  • Ensure API key belongs to the correct tenant
Missing Authorization Header

Request:

curl -X GET https://your-tenant.grizzlycbg.io/ks/keyrings

Response:

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "message": "Unauthorized"
}

Solution: Always include the Authorization header with Bearer token:

curl -X GET https://your-tenant.grizzlycbg.io/ks/keyrings \
  -H "Authorization: Bearer your-api-key"


Permission Errors (403)

Insufficient Permissions

Request:

curl -X POST https://your-tenant.grizzlycbg.io/ks/rotate \
  -H "Authorization: Bearer apikey-xxx" \
  -H "Content-Type: application/json" \
  -d '{"ringname": "Finance"}'

Response:

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "message": "Insufficient permissions to rotate keys for KeyRing 'Finance'"
}

Explanation: The API key doesn't have the required permission flag for this operation.

Required Permission: - keyring.Finance.rotate or keyring.*.rotate

Solution: Update the API key to include the required permission flag.


Bad Request Errors (400)

Missing Required Field

Request:

curl -X POST https://your-tenant.grizzlycbg.io/ks/keyrings \
  -H "Authorization: Bearer apikey-xxx" \
  -H "Content-Type: application/json" \
  -d '{}'

Response:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "message": "Missing required field: name"
}

Explanation: The name field is required when creating a KeyRing.

Correct Request:

curl -X POST https://your-tenant.grizzlycbg.io/ks/keyrings \
  -H "Authorization: Bearer apikey-xxx" \
  -H "Content-Type: application/json" \
  -d '{"name": "Finance"}'

Invalid JSON

Request:

curl -X POST https://your-tenant.grizzlycbg.io/ks/keyrings \
  -H "Authorization: Bearer apikey-xxx" \
  -H "Content-Type: application/json" \
  -d '{"name": "Finance"'

Response:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "message": "Invalid JSON in request body"
}

Explanation: The JSON payload is malformed (missing closing brace).

Solution: Ensure JSON is properly formatted and valid.

Missing Content-Type Header

Request:

curl -X POST https://your-tenant.grizzlycbg.io/ks/keyrings \
  -H "Authorization: Bearer apikey-xxx" \
  -d '{"name": "Finance"}'

Response:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "message": "Content-Type must be application/json"
}

Solution: Always include Content-Type: application/json header for POST/PUT requests.

Password Validation Failed

Request:

curl -X POST https://your-tenant.grizzlycbg.io/auth/user/invite-complete \
  -H "Content-Type: application/json" \
  -d '{
    "token": "invite-token-123",
    "password": "weak"
  }'

Response:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "state": "malformed-password",
  "results": {
    "isValid": false,
    "details": {
      "minLength": 8,
      "newPasswordLength": 4,
      "minUppercase": 1,
      "newPasswordUppercase": 0,
      "minLowercase": 1,
      "newPasswordLowercase": 4,
      "minSpecialChar": 1,
      "newPasswordSpecialChar": 0,
      "minNumber": 1,
      "newPasswordNumber": 0
    }
  }
}

Explanation: Password doesn't meet complexity requirements.

Password Requirements:

  • Minimum length: 8 characters
  • At least 1 uppercase letter
  • At least 1 lowercase letter
  • At least 1 number
  • At least 1 special character

Not Found Errors (404)

KeyRing Not Found

Request:

curl -X GET https://your-tenant.grizzlycbg.io/ks/keyrings/NonExistentRing \
  -H "Authorization: Bearer apikey-xxx"

Response:

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "message": "KeyRing 'NonExistentRing' not found"
}

Common Causes:

  • KeyRing name is misspelled
  • KeyRing doesn't exist
  • Using wrong case (though names are case-insensitive)

Solution:

  • List all KeyRings: GET /ks/keyrings
  • Verify KeyRing name spelling
  • Create the KeyRing if it doesn't exist
User Not Found

Request:

curl -X GET https://your-tenant.grizzlycbg.io/auth/users/nonexistent \
  -H "Authorization: Bearer apikey-xxx"

Response:

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "message": "User 'nonexistent' not found"
}

Solution:

  • Verify username is correct
  • Check if using email instead of username (use ?email=true query parameter)
  • List all users: GET /auth/users
Account Not Found

Request:

curl -X GET https://your-tenant.grizzlycbg.io/auth/accounts/invalid-id \
  -H "Authorization: Bearer apikey-xxx"

Response:

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "message": "Account not found"
}

Solution:

  • Verify Account ID is correct
  • Use Account UID if applicable: GET /auth/accounts/{id}?uid=true

Conflict Errors (409)

Asset Relationship Cycle

Request:

curl -X PATCH https://your-tenant.grizzlycbg.io/assets/asset-c \
  -H "Authorization: Bearer apikey-xxx" \
  -H "Content-Type: application/json" \
  -d '{"parents": ["asset-a"], "parentMode": "add"}'

Response:

HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "message": "Failed to update Asset \"asset-c\". This parent set would create a relationship cycle."
}

Explanation: Asset parent relationships form a directed acyclic graph — an asset can never become its own ancestor, directly or indirectly. This request would have made asset-a reachable from itself through asset-c. Nothing was changed.

Solution:

  • Check the target asset's existing family (GET /assets/:id/family) before re-parenting it
  • If two assets genuinely derive from each other, model it with a shared parent asset or a single canonical direction instead of a cycle — see Asset Tagging: Relationships Form a DAG

Validation Errors (422)

Asset Validation Failed

Request:

curl -X POST https://your-tenant.grizzlycbg.io/ks/decrypt \
  -H "Authorization: Bearer apikey-xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "hash": "invalid-hash",
    "header": "tampered-header"
  }'

Response:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "ringId": "ring-123",
  "ringname": "finance",
  "key": {
    "type": "aes256",
    "id": "key-456"
  },
  "asset": {
    "valid": false,
    "id": "asset-789",
    "message": "Asset signature verification failed",
    "data": {}
  }
}

Explanation: The encrypted data's asset metadata has been tampered with or is corrupted. The cryptographic signature doesn't match.

Common Causes:

  • Data was modified after encryption
  • Header was separated from payload
  • Encoding issues during storage/transmission

Solution:

  • Ensure encrypted data is stored and retrieved without modification
  • Verify no middleware is altering the data
  • Check for encoding/decoding issues (base64, etc.)

Server Errors (500)

Internal Server Error

Request:

curl -X POST https://your-tenant.grizzlycbg.io/ks/rotate \
  -H "Authorization: Bearer apikey-xxx" \
  -H "Content-Type: application/json" \
  -d '{"ringname": "Finance"}'

Response:

HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
  "message": "An unexpected error occurred while rotating the key"
}

Explanation: An unexpected server-side error occurred.

What to Do:

  1. Retry the request after a brief delay
  2. Check if the issue persists
  3. Review request parameters for potential issues
  4. Contact support if the error continues
  5. Provide the request details and timestamp for investigation

Resource-Specific Errors

KeyRings

Duplicate KeyRing Name (400)

{
  "message": "The KeyRing 'Finance' already exists. KeyRings must have unique names."
}

KeyRing Deletion Failed (400)

{
  "message": "Cannot delete KeyRing. Active API keys are still using this KeyRing."
}

API Keys

API Key Not Found (404)

{
  "message": "API Key not found"
}

Account Not Found When Creating API Key (404)

{
  "message": "Account not found. Cannot create API key."
}

Users

Duplicate Username (400)

{
  "message": "User with username 'john.doe' already exists"
}

Duplicate Email (400)

{
  "message": "User with email '[email protected]' already exists",
  "errors": {
    "email": "Email already registered"
  }
}

Invitations

Invalid Invite Token (401)

{
  "state": "invalid"
}

Expired Invite Token (401)

{
  "state": "expired"
}

Login

Invalid Credentials (400)

{
  "message": "Invalid username or password"
}

SDK Error Types (GrizzlyError)

When you use the Grizzly SDK, you do not parse these HTTP responses yourself. The asset, activity, and admin surfaces (client.assets, client.activities, client.admin, client.bulk) raise a typed GrizzlyError hierarchy — never a bare Error(message) or a swallowed undefined — so you branch on the error class or a stable machine code instead of matching message strings.

Error shape

Every GrizzlyError carries:

Field Type Description
status number | undefined HTTP status, when the error came from a response
code string Stable machine code — one of validation, unauthorized, forbidden, not_found, rate_limited, server_error, network (status/transport-derived), or the SDK-local decrypt.failed, tag_schema.violation, aborted. Survives serialization; safe to branch on
retryable boolean true when retrying the same request with backoff is safe
requestId string | undefined The x-request-id sent with the call, for correlating to a server Activity

Specializations

The right subclass is built from the response status. Branch with instanceof:

Class Status Notes
AuthError 401 / 403 API key missing, invalid, or lacking the required entitlement
NotFoundError 404 The addressed resource does not exist
ValidationError 400 Rejected as invalid (bad body, bad cursor). Also thrown client-side by tag-schema validation, before the request hits the wire (code tag_schema.violation)
RateLimitError 429 Rate limited. retryAfterMs carries the server's Retry-After when present
ServerError 5xx / network Server-side or connect failure. retryable: true
GrizzlyError any other, including 409 The base class, used when no specialization applies. A rejected assets.update parent change that would create a relationship cycle surfaces this way — check err.status === 409
import { NotFoundError, RateLimitError, AuthError, ValidationError } from '@grizzlycbg/grizzly-sdk'

try {
   // assets.family is the one asset surface that 404s on a missing asset
   await client.assets.family('asset-123')
   await client.assets.update('asset-123', { data: { state: 'approved' } })
} catch (err) {
   if (err instanceof NotFoundError) {
      // 404 from assets.family — the asset does not exist
   } else if (err instanceof RateLimitError) {
      // honor the server's backoff hint when present
      await new Promise((r) => setTimeout(r, err.retryAfterMs ?? 1000))
   } else if (err instanceof ValidationError) {
      // 400 from assets.update — the ADDRESSED asset is missing (a missing referenced
      // *parent* id is a 404/NotFoundError instead). Inspect err.code / err.message for the cause
   } else if (err instanceof AuthError) {
      // 401/403 — re-check the API key and its entitlements
   } else {
      throw err
   }
}

assets.get returns undefined for a missing asset rather than throwing NotFoundError. Of the other surfaces, assets.family and assets.update both throw NotFoundError (404) for a missing asset — assets.update raises the same 404 whether the missing id is the target or a referenced parent, distinguished by the error message — and assets.delete is idempotent, returning without throwing when the asset is already gone.

Retry semantics

The SDK transport retries automatically with exponential backoff and full jitter (default 2 retries — up to 3 attempts):

  • 429 and 503 are always retried, honoring the Retry-After header (delta-seconds or an HTTP date) surfaced as RateLimitError.retryAfterMs.
  • Other 5xx and connect/network errors are retried only for idempotent requests.
  • Idempotency gating: GET and DELETE are idempotent by default; POST and PATCH are not. A non-idempotent write that already received a definitive response is never silently replayed — for example a failed PATCH /assets/:id (which records an update activity on every apply) is surfaced to you rather than double-applied.
  • Timeouts & cancellation: each attempt is bounded by a per-attempt timeout (default 30s, tunable via KeyClientOptions.http.requestTimeoutMs); a timed-out attempt is a retryable connect-class failure (idempotent-gated), exactly like a connect error. An AbortSignal passed to a call (e.g. assets.family(id, { signal }), assets.update(id, { …, signal })) cancels it immediately — including any pending backoff — and throws a GrizzlyError with code: 'aborted' and retryable: false; an aborted request is never retried.

Error Handling Best Practices

1. Always Check Status Codes

const response = await fetch('https://your-tenant.grizzlycbg.io/ks/keyrings', {
   headers: {
      'Authorization': `Bearer ${apiKey}`
   }
})

if (!response.ok) {
   const error = await response.json()
   console.error(`API Error (${response.status}):`, error.message)

   // Handle specific error codes
   if (response.status === 401) {
      // Refresh authentication
   } else if (response.status === 403) {
      // Check permissions
   } else if (response.status === 404) {
      // Resource doesn't exist
   }
}

2. Implement Retry Logic

async function apiCallWithRetry(url, options, maxRetries = 3) {
   for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
         const response = await fetch(url, options)

         if (response.ok) {
            return await response.json()
         }

         // Don't retry client errors (4xx)
         if (response.status >= 400 && response.status < 500) {
            const error = await response.json()
            throw new Error(error.message)
         }

         // Retry server errors (5xx)
         if (response.status >= 500 && attempt < maxRetries) {
            const delay = Math.pow(2, attempt) * 1000 // Exponential backoff
            await new Promise(resolve => setTimeout(resolve, delay))
            continue
         }

         const error = await response.json()
         throw new Error(error.message)

      } catch (error) {
         if (attempt === maxRetries) {
            throw error
         }
      }
   }
}

3. Parse Error Messages

async function createKeyRing(name) {
   try {
      const response = await fetch('https://your-tenant.grizzlycbg.io/ks/keyrings', {
         method: 'POST',
         headers: {
            'Authorization': `Bearer ${apiKey}`,
            'Content-Type': 'application/json'
         },
         body: JSON.stringify({ name })
      })

      if (!response.ok) {
         const error = await response.json()

         // Handle specific error scenarios
         if (error.message.includes('already exists')) {
            console.log('KeyRing already exists, fetching existing one...')
            return await getKeyRing(name)
         }

         if (error.message.includes('maximum number')) {
            console.error('Plan limit reached:', error.message)
            // Prompt user to upgrade plan
            return null
         }

         throw new Error(error.message)
      }

      return await response.json()

   } catch (error) {
      console.error('Failed to create KeyRing:', error.message)
      throw error
   }
}

4. Log Errors for Debugging

async function makeApiCall(url, options) {
   const requestId = crypto.randomUUID()

   console.log(`[${requestId}] Request:`, {
      url,
      method: options.method,
      timestamp: new Date().toISOString()
   })

   try {
      const response = await fetch(url, options)

      if (!response.ok) {
         const error = await response.json()

         console.error(`[${requestId}] Error Response:`, {
            status: response.status,
            message: error.message,
            timestamp: new Date().toISOString()
         })

         throw new Error(error.message)
      }

      console.log(`[${requestId}] Success:`, {
         status: response.status,
         timestamp: new Date().toISOString()
      })

      return await response.json()

   } catch (error) {
      console.error(`[${requestId}] Request Failed:`, error.message)
      throw error
   }
}

Next Steps