Error Handling
Understand API error responses and implement robust error handling
Error Handling
The Seal API uses standard HTTP status codes and returns detailed error responses following RFC 7807 (Problem Details for HTTP APIs) to help you diagnose and handle errors effectively.
Error Response Format
All error responses follow this consistent structure:
{
"type": "https://seal.nyc/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "Invalid email format for recipient",
"instance": "/api/v1/documents/abc123/recipients"
}Response Fields
| Field | Type | Description |
|---|---|---|
type | string | URI reference identifying the error type |
title | string | Short, human-readable summary of the error |
status | number | HTTP status code |
detail | string | Detailed explanation of the specific error |
instance | string | URI reference to the specific request that caused the error |
Common Error Codes
400 Bad Request
The request was malformed or contains invalid parameters.
Common Causes:
- Missing required fields in request body
- Invalid JSON syntax
- Malformed data (e.g., invalid email format, invalid date)
- Invalid enum values (e.g., unknown recipient role)
Example:
{
"type": "https://seal.nyc/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "Field 'email' is required for recipient",
"instance": "/api/v1/documents/abc123/recipients"
}401 Unauthorized
Authentication failed or credentials are missing.
Common Causes:
- Missing
Authorizationheader - Invalid API key format
- Expired API key
- Malformed Bearer token
Example:
{
"type": "https://seal.nyc/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid or expired API key",
"instance": "/api/v1/documents"
}403 Forbidden
Authentication succeeded but you don't have permission to perform this action.
Common Causes:
- API key lacks required scope (e.g., trying to write with read-only scope)
- User is not a member of the organization
- User's role doesn't have the required permission
- Resource belongs to a different organization
Example:
{
"type": "https://seal.nyc/errors/insufficient-scope",
"title": "Forbidden",
"status": 403,
"detail": "Missing required scope: seal:documents:write",
"instance": "/api/v1/documents"
}404 Not Found
The requested resource doesn't exist.
Common Causes:
- Invalid document/recipient/template ID
- Resource was deleted
- Typo in the endpoint URL
- Resource belongs to a different organization
Example:
{
"type": "https://seal.nyc/errors/not-found",
"title": "Not Found",
"status": 404,
"detail": "Document not found: abc123",
"instance": "/api/v1/documents/abc123"
}429 Too Many Requests
You've exceeded the rate limit for API requests.
Common Causes:
- Too many requests in a short time window
- Burst traffic exceeding rate limits
Example:
{
"type": "https://seal.nyc/errors/rate-limit",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded. Retry after 60 seconds.",
"instance": "/api/v1/documents"
}Retry Strategy:
- Wait before retrying (use exponential backoff)
- Check
Retry-Afterheader if present - Implement request queuing to stay within limits
500 Internal Server Error
An unexpected error occurred on the server.
Common Causes:
- Temporary server issues
- Database connectivity problems
- Unexpected internal state
Example:
{
"type": "https://seal.nyc/errors/internal-error",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred. Please try again later.",
"instance": "/api/v1/documents/abc123/send"
}Retry Strategy:
- Retry with exponential backoff
- Maximum 3 retry attempts
- Log the error for investigation
Error Handling Best Practices
TypeScript Example
interface ApiError {
type: string;
title: string;
status: number;
detail: string;
instance: string;
}
async function sendDocument(documentId: string, message: string) {
try {
const response = await fetch(`https://seal.convex.site/api/v1/documents/${documentId}/send`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEAL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ message }),
});
if (!response.ok) {
const error: ApiError = await response.json();
// Handle specific error types
switch (error.status) {
case 400:
console.error("Invalid request:", error.detail);
throw new Error(`Validation failed: ${error.detail}`);
case 401:
console.error("Authentication failed:", error.detail);
// Refresh API key or re-authenticate
throw new Error("Authentication required");
case 403:
console.error("Permission denied:", error.detail);
throw new Error(`Insufficient permissions: ${error.detail}`);
case 404:
console.error("Document not found:", error.detail);
throw new Error("Document does not exist");
case 429:
console.error("Rate limit exceeded. Retrying after delay...");
// Implement exponential backoff
await new Promise((resolve) => setTimeout(resolve, 60000));
return sendDocument(documentId, message); // Retry
case 500:
console.error("Server error:", error.detail);
// Retry with exponential backoff
throw new Error("Server error. Please try again later.");
default:
console.error("Unexpected error:", error);
throw new Error(error.detail);
}
}
return await response.json();
} catch (error) {
// Handle network errors
if (error instanceof TypeError && error.message.includes("fetch")) {
console.error("Network error:", error);
throw new Error("Network connection failed");
}
throw error;
}
}Exponential Backoff Implementation
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000,
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const isLastAttempt = attempt === maxRetries - 1;
const shouldRetry =
error instanceof Error && (error.message.includes("500") || error.message.includes("429"));
if (isLastAttempt || !shouldRetry) {
throw error;
}
// Exponential backoff: 1s, 2s, 4s, 8s...
const delay = baseDelay * Math.pow(2, attempt);
console.log(`Retry attempt ${attempt + 1} after ${delay}ms`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error("Max retries exceeded");
}
// Usage
const result = await retryWithBackoff(() => sendDocument("doc123", "Please sign"));Validation Errors
For 400 errors, the detail field often contains specific validation information:
{
"type": "https://seal.nyc/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "Validation failed: email must be a valid email address, role must be one of: signer, approver, viewer",
"instance": "/api/v1/documents/abc123/recipients"
}Tip: Always log the full error response (including type and instance) to help with
debugging. The instance field shows exactly which request failed.
Best Practices
-
Always Check Response Status: Don't assume requests succeed. Check
response.okor status code. -
Parse Error Responses: Extract the
detailfield for user-friendly error messages. -
Implement Retry Logic: Use exponential backoff for 429 and 500 errors.
-
Log Errors: Include
type,status,detail, andinstancein logs for debugging. -
Handle Network Errors: Catch network failures separately from API errors.
-
User-Friendly Messages: Transform technical error details into actionable messages for end users.
-
Monitor Error Rates: Track error responses to identify integration issues early.
Next Steps
- API Reference - Explore all endpoints and their specific error conditions
- Webhooks - Set up error notifications via webhooks
- Authentication - Review authentication requirements and scope errors
Last updated on