Error Handling
SignStack uses conventional HTTP response codes to indicate the success or failure of API requests. This guide covers error responses, common causes, and how to handle them in your applications.
HTTP Status Codes
| Code | Meaning |
|---|---|
200 |
Success - Request completed successfully |
201 |
Created - Resource was created successfully |
400 |
Bad Request - Invalid request parameters or body |
401 |
Unauthorized - Missing or invalid authentication |
403 |
Forbidden - Valid auth, but insufficient permissions |
404 |
Not Found - Resource doesn't exist |
409 |
Conflict - Resource state conflict (e.g., concurrent edit) |
422 |
Unprocessable Entity - Validation error |
429 |
Too Many Requests - Rate limit exceeded |
500 |
Internal Server Error - Something went wrong on our end |
Error Response Format
Error responses include a JSON body with details:
{
"error": {
"code": "validation_error",
"message": "The request body is invalid",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
}
]
}
}
| Field | Description |
|---|---|
error.code |
Machine-readable error code |
error.message |
Human-readable description |
error.details |
Array of specific issues (when applicable) |
Common Error Codes
Authentication Errors (401)
invalid_token
{
"error": {
"code": "invalid_token",
"message": "The access token is invalid or expired"
}
}
Causes:
- Token has expired (tokens are valid for ~1 hour)
- Token was revoked
- Malformed Authorization header
Solution: Exchange your API key for a fresh token:
POST /v1/auth/token
{ "grantType": "api_key", "apiKey": "sk_live_..." }
Permission Errors (403)
insufficient_scope
{
"error": {
"code": "insufficient_scope",
"message": "Token lacks required scope: workflow:write"
}
}
Causes:
- API key doesn't have the required scopes
- Trying to access resources from another organization
Solution: Create a new API key with the required scopes in your dashboard.
Validation Errors (400, 422)
validation_error
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": [
{ "field": "participants[0].email", "message": "Required field is missing" },
{ "field": "entitySlots[1].schemaKey", "message": "Schema 'invalid-schema' not found" }
]
}
}
Causes:
- Missing required fields
- Invalid field types or formats
- Reference to non-existent resources (schemas, templates, etc.)
Solution: Check the details array for specific field issues.
Resource Not Found (404)
not_found
{
"error": {
"code": "not_found",
"message": "Workflow wf_abc123 not found"
}
}
Causes:
- Resource was deleted
- Incorrect ID or key
- Resource belongs to different organization
Solution: Verify the resource ID and your organization context.
Conflict Errors (409)
conflict
{
"error": {
"code": "conflict",
"message": "Resource was modified by another request",
"details": [
{ "field": "updatedAt", "message": "Expected 2024-01-15T10:00:00Z but resource was updated at 2024-01-15T10:05:00Z" }
]
}
}
Causes:
- Concurrent updates to the same resource
- Stale
updatedAttimestamp in request
Solution: Re-fetch the resource, merge your changes, and retry with the current updatedAt value.
Rate Limit Errors (429)
rate_limit_exceeded
{
"error": {
"code": "rate_limit_exceeded",
"message": "Too many requests"
}
}
Headers included:
Retry-After: Seconds to wait before retryingX-RateLimit-Limit: Request limit per windowX-RateLimit-Remaining: Remaining requests in window
Solution: Implement exponential backoff and respect the Retry-After header.
Handling Errors in Code
JavaScript/TypeScript
async function createWorkflow(data: CreateWorkflowReq): Promise<Workflow> {
const response = await fetch(`https://api.signstack.ai/v1/orgs/${orgId}/workflows`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
switch (response.status) {
case 401:
// Token expired - refresh and retry
await refreshToken();
return createWorkflow(data);
case 422:
// Validation error - log details for debugging
console.error('Validation failed:', error.error.details);
throw new ValidationError(error.error.message, error.error.details);
case 429:
// Rate limited - wait and retry
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
await sleep(retryAfter * 1000);
return createWorkflow(data);
default:
throw new ApiError(error.error.message, response.status);
}
}
return response.json();
}
Python
import requests
import time
def create_workflow(data):
response = requests.post(
f'https://api.signstack.ai/v1/orgs/{org_id}/workflows',
headers={'Authorization': f'Bearer {access_token}'},
json=data
)
if response.status_code == 401:
# Token expired - refresh and retry
refresh_token()
return create_workflow(data)
if response.status_code == 429:
# Rate limited - wait and retry
retry_after = int(response.headers.get('Retry-After', 60))
time.sleep(retry_after)
return create_workflow(data)
if not response.ok:
error = response.json().get('error', {})
raise ApiError(error.get('message'), response.status_code)
return response.json()
Best Practices
1. Implement Retry Logic
For transient errors (429, 500, 502, 503, 504), implement exponential backoff:
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;
if (!isRetryable(error)) throw error;
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await sleep(delay);
}
}
}
2. Handle Token Expiration
Access tokens expire after ~1 hour. Proactively refresh before expiration:
function isTokenExpiringSoon(expiresAt: string): boolean {
const expiry = new Date(expiresAt);
const buffer = 5 * 60 * 1000; // 5 minutes
return Date.now() > expiry.getTime() - buffer;
}
3. Log Error Details
Always log the full error response for debugging:
if (!response.ok) {
const error = await response.json();
console.error('API Error:', {
status: response.status,
code: error.error?.code,
message: error.error?.message,
details: error.error?.details,
requestId: response.headers.get('X-Request-Id')
});
}
4. Use Optimistic Locking
For update operations, include the updatedAt timestamp to detect concurrent modifications:
const workflow = await getWorkflow(workflowId);
await updateWorkflow(workflowId, {
...changes,
updatedAt: workflow.updatedAt // Include for conflict detection
});
5. Validate Before Sending
Catch obvious errors client-side before making API calls:
function validateWorkflowRequest(data: CreateWorkflowReq): void {
if (!data.blueprintKey && !data.documents?.length) {
throw new Error('Either blueprintKey or documents required');
}
for (const participant of data.participants ?? []) {
if (!participant.email?.includes('@')) {
throw new Error(`Invalid email for participant: ${participant.email}`);
}
}
}
Webhook Error Handling
For webhook deliveries, SignStack retries failed deliveries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
Your webhook endpoint should:
- Return
2xxstatus for successful processing - Return
4xxfor invalid/rejected events (won't retry) - Implement idempotency using the
eventIdfield
app.post('/webhooks/signstack', async (req, res) => {
const { eventId, eventType, data } = req.body;
// Check if already processed
if (await isEventProcessed(eventId)) {
return res.status(200).json({ status: 'already_processed' });
}
try {
await processEvent(eventType, data);
await markEventProcessed(eventId);
res.status(200).json({ status: 'processed' });
} catch (error) {
console.error('Webhook processing failed:', error);
res.status(500).json({ status: 'error' }); // Triggers retry
}
});