Skip to content

Error Handling

Consistent error format across all endpoints.

Error format

All API errors return a JSON object with an error field containing code and message. Validation errors include an additional details field with per-field errors.

error-response.json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request body",
    "details": {
      "actor": ["actor is required"],
      "action": ["action is required"]
    }
  }
}

Error codes

Status Code Description Resolution
400 VALIDATION_ERROR Request body or query parameters failed validation Check the details field for specific field errors
400 INVALID_CURSOR The pagination cursor is malformed or expired Use a fresh cursor from a previous response
401 UNAUTHORIZED Missing or invalid API key Include a valid API key in the Authorization header
403 FORBIDDEN API key does not have permission for this action Check your key permissions in the dashboard
404 NOT_FOUND The requested resource does not exist Verify the event or chain ID is correct
409 CONFLICT Concurrent write conflict on the hash chain Retry the request — the chain position was claimed by another write
403 ORG_REQUIRED Request requires an organization context Ensure your Clerk session or API key is associated with an organization
404 TENANT_NOT_FOUND No tenant found for the given organization Run POST /v1/tenants/setup to provision your tenant first
429 RATE_LIMITED Too many requests (coming soon — not yet enforced) Back off and retry with exponential delay
429 QUOTA_EXCEEDED Monthly event quota exceeded for your plan Upgrade your plan or wait for the next billing cycle
500 INTERNAL_ERROR Unexpected server error Retry once, then contact support with the request ID

Validation errors

Request validation is powered by Zod. When validation fails, the details object maps field names to arrays of error messages.

validation-error.json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request body",
    "details": {
      "actor": ["String must contain at least 1 character(s)"],
      "context": ["Expected object, received string"]
    }
  }
}

Quotas and limits

Each plan has a monthly event quota:

Plan Monthly events Retention
Free 2,500 30 days
Pro 100,000 1 year
Business Unlimited Unlimited

When you exceed your quota, the API returns 429 QUOTA_EXCEEDED.

Rate limiting (429 RATE_LIMITED) will be enforced in a future release. Plan your integration accordingly.

Retry strategy

The 409 CONFLICT error occurs during concurrent writes to the same hash chain. Each event must reference the previous event's hash — when two requests race for the same chain position, one wins and the other gets a 409. This is expected under high concurrency.

How to retry: simply resubmit the same request. The API will automatically fetch the latest chain state and assign the next available position. No need to re-fetch chain status yourself — the retry is transparent.

For 409 conflicts, retry immediately — no backoff needed. The retry will get the next available chain position. For 429 rate limits, use exponential backoff starting at 1 second.
retry.ts
async function logEventWithRetry(payload: object, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const res = await fetch("https://api.sealtrail.dev/v1/events", {
      method: "POST",
      headers: {
        "Authorization": "Bearer stl_live_...",
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    if (res.status === 409) continue; // Retry immediately
    if (res.status === 429) {
      await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
      continue;
    }

    return res.json();
  }
  throw new Error("Max retries exceeded");
}