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": {
"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.
{
"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.
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.
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.
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");
}