notiflowsDocs

Error Handling

Understanding and handling API errors in Notiflows

Error Handling

All Notiflows API errors follow a consistent JSON structure, making it easy to handle errors programmatically across both the Admin API and User API.

Error Response Format

Every non-2xx response returns a JSON object with the following structure:

{
  "error": {
    "type": "https://docs.notiflows.com/errors/not_found",
    "code": "not_found",
    "message": "User not found",
    "status": 404,
    "request_id": "req_abc123xyz",
    "details": {
      "resource": "user",
      "id": "user_123"
    }
  }
}

Fields

FieldTypeDescription
typestringURL to documentation for this error type
codestringStable, machine-readable error code (see Error Codes)
messagestringHuman-readable error message
statusintegerHTTP status code
request_idstring | nullCorrelation ID for debugging (from x-request-id header)
detailsobjectAdditional context specific to the error type

The code field is stable and safe to use for programmatic error handling. The message field may change and should only be used for display purposes.

Error Codes

Notiflows uses a small, stable set of top-level error codes. Business-specific context is provided in the details object.

bad_request (400)

Invalid JSON syntax or malformed request parameters.

{
  "error": {
    "type": "https://docs.notiflows.com/errors/bad_request",
    "code": "bad_request",
    "message": "Invalid UUID format",
    "status": 400,
    "request_id": "req_abc123",
    "details": {
      "reason": "invalid_uuid",
      "param": "channel_id"
    }
  }
}

Details fields:

FieldTypeDescription
reasonstringMachine-readable reason: invalid_json, invalid_param, invalid_uuid
paramstringParameter that caused the error (optional)

unauthenticated (401)

Missing or invalid authentication credentials.

{
  "error": {
    "type": "https://docs.notiflows.com/errors/unauthenticated",
    "code": "unauthenticated",
    "message": "Authentication required",
    "status": 401,
    "request_id": "req_abc123",
    "details": {
      "auth": "api_key",
      "reason": "missing_api_key"
    }
  }
}

Details fields:

FieldTypeDescription
authstringAuthentication method: api_key, secret_key, user_key_jwt
reasonstringSpecific reason (optional)

Common reasons:

ReasonDescription
missing_api_keyx-notiflows-api-key header not provided
invalid_api_keyAPI key doesn't match any project
missing_secret_keyx-notiflows-secret-key header not provided (Admin API)
invalid_secret_keySecret key doesn't match the project
missing_user_keyx-notiflows-user-key header not provided (User API with Security Mode)
invalid_signatureJWT signature verification failed
signing_key_not_configuredSecurity Mode enabled but no signing key generated
user_not_foundUser from JWT doesn't exist in the project

forbidden (403)

Authenticated but not authorized to access the resource.

{
  "error": {
    "type": "https://docs.notiflows.com/errors/forbidden",
    "code": "forbidden",
    "message": "Access denied",
    "status": 403,
    "request_id": "req_abc123",
    "details": {
      "resource": "channel",
      "id": "ch_abc123",
      "required_scope": "channel:write"
    }
  }
}

Details fields:

FieldTypeDescription
resourcestringResource type being accessed
idstringResource ID (optional)
required_scopestringPermission scope required (optional)

The User API uses not_found instead of forbidden when revealing resource existence would leak cross-tenant information. This is a security measure to prevent enumeration attacks.


not_found (404)

The requested resource does not exist.

{
  "error": {
    "type": "https://docs.notiflows.com/errors/not_found",
    "code": "not_found",
    "message": "User not found",
    "status": 404,
    "request_id": "req_abc123",
    "details": {
      "resource": "user",
      "id": "user_123"
    }
  }
}

Details fields:

FieldTypeDescription
resourcestringResource type: user, channel, notiflow, notification, delivery, topic, subscription, feed_entry
idstringResource ID (optional)

conflict (409)

The request conflicts with the current state of the resource. This includes invalid state transitions, business rule violations, and duplicate resources.

{
  "error": {
    "type": "https://docs.notiflows.com/errors/conflict",
    "code": "conflict",
    "message": "Notiflow must be active to run",
    "status": 409,
    "request_id": "req_abc123",
    "details": {
      "resource": "notiflow",
      "id": "nf_abc123",
      "reason": "notiflow_not_active",
      "current_status": "draft",
      "allowed_statuses": ["active"]
    }
  }
}

Details fields:

FieldTypeDescription
resourcestringResource type
idstringResource ID (optional)
reasonstringMachine-readable reason code (required)
current_statusstringCurrent resource status (optional)
allowed_statusesarrayList of valid statuses (optional)
allowed_transitionsarrayList of valid state transitions (optional)

Common conflict reasons:

ReasonDescription
notiflow_not_activeAttempted to run a notiflow that isn't active
invalid_transitionAttempted an invalid state change (e.g., setting read to false)
subscription_already_existsUser is already subscribed to the topic
provider_not_configuredChannel provider (e.g., Slack) not set up
provider_revokedProvider access has been revoked

idempotency_key_reused (409)

The same idempotency key was used with a different request payload.

{
  "error": {
    "type": "https://docs.notiflows.com/errors/idempotency_key_reused",
    "code": "idempotency_key_reused",
    "message": "Idempotency key already used with different payload",
    "status": 409,
    "request_id": "req_abc123",
    "details": {
      "idempotency_key": "user_action_123",
      "resource": "notiflow_run"
    }
  }
}

Details fields:

FieldTypeDescription
idempotency_keystringThe reused idempotency key
resourcestringResource type the key was used with

Idempotency keys are supported on action endpoints like POST /notiflows/:id/run. Using the same key with the same payload is safe and returns the original response.


validation_failed (422)

Request validation failed. This includes Ecto changeset errors and custom validation rules.

{
  "error": {
    "type": "https://docs.notiflows.com/errors/validation_failed",
    "code": "validation_failed",
    "message": "Validation failed",
    "status": 422,
    "request_id": "req_abc123",
    "details": {
      "fields": [
        {
          "field": "email",
          "reason": "format",
          "message": "must be a valid email address"
        },
        {
          "field": "recipients.0.external_id",
          "reason": "required",
          "message": "can't be blank"
        }
      ]
    }
  }
}

Details fields:

FieldTypeDescription
fieldsarrayList of field-level errors

Field error structure:

FieldTypeDescription
fieldstringDot-path to the field (e.g., recipients.0.external_id)
reasonstringMachine-readable validation reason
messagestringHuman-readable error message

Common validation reasons:

ReasonDescription
requiredField is required but missing
formatField format is invalid
inclusionValue not in allowed list
exclusionValue in excluded list
lengthString length constraint violated
numberNumeric constraint violated
uniqueUniqueness constraint violated

rate_limited (429)

Too many requests. The response includes a Retry-After header.

{
  "error": {
    "type": "https://docs.notiflows.com/errors/rate_limited",
    "code": "rate_limited",
    "message": "Rate limit exceeded",
    "status": 429,
    "request_id": "req_abc123",
    "details": {
      "retry_after": 60,
      "limit": 100,
      "window": "1m"
    }
  }
}

Details fields:

FieldTypeDescription
retry_afterintegerSeconds to wait before retrying
limitintegerRequest limit (optional)
windowstringTime window for the limit (optional)

Response headers:

HeaderDescription
Retry-AfterSeconds to wait before retrying

internal_error (500)

An unexpected server error occurred. These errors are logged and monitored.

{
  "error": {
    "type": "https://docs.notiflows.com/errors/internal_error",
    "code": "internal_error",
    "message": "Internal server error",
    "status": 500,
    "request_id": "req_abc123",
    "details": {}
  }
}

Internal errors never expose stack traces or internal details. If you encounter persistent 500 errors, contact support with the request_id for investigation.


service_unavailable (503)

A dependent service is temporarily unavailable.

{
  "error": {
    "type": "https://docs.notiflows.com/errors/service_unavailable",
    "code": "service_unavailable",
    "message": "Service temporarily unavailable",
    "status": 503,
    "request_id": "req_abc123",
    "details": {}
  }
}

Handling Errors in Code

JavaScript/TypeScript

The Notiflows client SDK provides typed error classes for each error code:

import {
  NotiflowsClient,
  NotFoundError,
  ValidationFailedError,
  ConflictError,
  UnauthenticatedError,
  isApiError
} from '@notiflows/client';

const client = new NotiflowsClient({ apiKey: 'pk_...', userToken: '...' });

try {
  await client.feed.markAsRead('entry_123');
} catch (error) {
  if (error instanceof NotFoundError) {
    console.log(`Entry not found: ${error.resource} ${error.resourceId}`);
  } else if (error instanceof ValidationFailedError) {
    // Access field-level errors
    for (const field of error.fields) {
      console.log(`${field.field}: ${field.message} (${field.reason})`);
    }
  } else if (error instanceof ConflictError) {
    // Check specific conflict reasons
    if (error.reason === 'invalid_transition') {
      console.log('Cannot undo this action');
    }
  } else if (error instanceof UnauthenticatedError) {
    // Redirect to login or refresh token
    if (error.reason === 'token_expired') {
      await refreshToken();
    }
  } else if (isApiError(error)) {
    // Handle any other API error
    console.log(`API error: ${error.code} - ${error.message}`);
  } else {
    // Network error or other issue
    throw error;
  }
}

Using Type Guards

import { isApiError, isConflictWithReason, isNotFoundResource } from '@notiflows/client';

try {
  await client.notiflows.run('nf_123', { recipients: [...] });
} catch (error) {
  if (isConflictWithReason(error, 'notiflow_not_active')) {
    // Handle inactive notiflow specifically
    console.log('Please activate the notiflow first');
  } else if (isNotFoundResource(error, 'notiflow')) {
    // Handle missing notiflow
    console.log('Notiflow does not exist');
  }
}

React Hook Error Handling

import { useFeed, NotFoundError, UnauthenticatedError } from '@notiflows/react';

function NotificationFeed() {
  const { entries, error, isLoading, retry } = useFeed();

  if (error) {
    if (error instanceof UnauthenticatedError) {
      return <LoginPrompt />;
    }

    return (
      <ErrorMessage
        message={error.message}
        onRetry={retry}
      />
    );
  }

  if (isLoading) {
    return <Loading />;
  }

  return <NotificationList entries={entries} />;
}

HTTP Status Code Summary

StatusError CodeWhen Used
400bad_requestInvalid JSON, malformed parameters, invalid UUIDs
401unauthenticatedMissing or invalid authentication
403forbiddenAuthenticated but not authorized
404not_foundResource doesn't exist
409conflictState conflict, business rule violation
409idempotency_key_reusedIdempotency key reused with different payload
422validation_failedRequest validation errors
429rate_limitedToo many requests
500internal_errorUnexpected server error
503service_unavailableDependent service down

Best Practices

Use Error Codes, Not Messages

Always match on the code field, never the message:

// ✅ Good - stable error code
if (error.code === 'not_found') { ... }

// ❌ Bad - message may change
if (error.message.includes('not found')) { ... }

Use Conflict Reasons for Business Logic

For conflict errors, use the reason field to determine specific handling:

// ✅ Good - specific reason handling
if (error.code === 'conflict' && error.details.reason === 'notiflow_not_active') {
  showActivateNotiflowPrompt();
}

// ❌ Bad - fragile message matching
if (error.message.includes('must be active')) { ... }

Include Request ID in Support Tickets

When reporting issues, always include the request_id from the error response. This helps us quickly locate the relevant logs.

Implement Exponential Backoff for Rate Limits

When receiving a rate_limited error, respect the Retry-After header:

async function withRetry(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (error instanceof RateLimitedError) {
        await sleep(error.retryAfter * 1000);
        continue;
      }
      throw error;
    }
  }
}

Troubleshooting

Getting Empty or Malformed Error Responses

If you receive an error response that doesn't match the expected format:

  1. Check that you're sending Accept: application/json header
  2. Verify the API endpoint URL is correct
  3. Check for network proxies that might modify responses

Unexpected 404 Errors

In the User API, 404 may be returned instead of 403 for security:

  • The resource might exist but belong to a different tenant
  • Verify you're using the correct API key and user token
  • Ensure the user has been created via the Admin API first

Persistent 500 Errors

If you encounter repeated internal errors:

  1. Note the request_id from each response
  2. Check the status page for known issues
  3. Contact support with the request IDs

On this page