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
messagefield 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
Bearerprefix 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=truequery 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:
- Retry the request after a brief delay
- Check if the issue persists
- Review request parameters for potential issues
- Contact support if the error continues
- 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.getreturnsundefinedfor a missing asset rather than throwingNotFoundError. Of the other surfaces,assets.familyandassets.updateboth throwNotFoundError(404) for a missing asset —assets.updateraises the same 404 whether the missing id is the target or a referenced parent, distinguished by the error message — andassets.deleteis 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):
429and503are always retried, honoring theRetry-Afterheader (delta-seconds or an HTTP date) surfaced asRateLimitError.retryAfterMs.- Other
5xxand connect/network errors are retried only for idempotent requests. - Idempotency gating:
GETandDELETEare idempotent by default;POSTandPATCHare not. A non-idempotent write that already received a definitive response is never silently replayed — for example a failedPATCH /assets/:id(which records anupdateactivity 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. AnAbortSignalpassed to a call (e.g.assets.family(id, { signal }),assets.update(id, { …, signal })) cancels it immediately — including any pending backoff — and throws aGrizzlyErrorwithcode: 'aborted'andretryable: 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
- Pagination - Handling paginated responses
- Troubleshooting - Common issues and solutions
- API Specification - Complete API reference