Skip to main content

Error Handling & Troubleshooting

Handle errors gracefully and debug issues effectively with this comprehensive guide.

Error Response Format

All API errors follow a consistent JSON structure:

{
"errorCode": "ENVELOPE_NOT_FOUND",
"message": "Envelope abc123 not found",
"requestId": "req_xyz789"
}
FieldDescription
errorCodeMachine-readable error identifier
messageHuman-readable description
requestIdUnique ID for support inquiries

HTTP Status Codes

StatusMeaningCommon Causes
200SuccessRequest completed
201CreatedResource created
204No ContentDeletion successful
400Bad RequestInvalid input, validation failed
401UnauthorizedInvalid or expired token
403ForbiddenInsufficient permissions
404Not FoundResource doesn't exist
409ConflictState conflict (e.g., already signed)
422UnprocessableValid syntax but semantic error
429Rate LimitedToo many requests
500Server ErrorInternal error (contact support)
503UnavailableTemporary outage

Error Code Reference

Authentication Errors

Error CodeHTTP StatusCauseSolution
UNAUTHORIZED401Missing or invalid Authorization headerInclude valid Bearer token
TOKEN_EXPIRED401Access token has expiredRefresh your token
INVALID_TOKEN401Malformed or revoked tokenRequest new token
INSUFFICIENT_SCOPE403Token lacks required scopesRequest token with correct scopes

Envelope Errors

Error CodeHTTP StatusCauseSolution
ENVELOPE_NOT_FOUND404Envelope ID doesn't exist or wrong accountVerify envelope ID and account
ENVELOPE_CANNOT_BE_MODIFIED400Envelope is sent/completed/voidedCan only modify drafts
ENVELOPE_ALREADY_SENT409Attempting to send already-sent envelopeCheck envelope status first
INVALID_ENVELOPE_STATUS400Invalid status transitionFollow valid status flow
DOCUMENTS_REQUIRED400No documents in envelopeAdd at least one document
RECIPIENTS_REQUIRED400No recipients definedAdd at least one recipient

Document Errors

Error CodeHTTP StatusCauseSolution
DOCUMENT_NOT_FOUND404Document ID doesn't existVerify document ID
INVALID_DOCUMENT_FORMAT400Unsupported file typeUse PDF format
DOCUMENT_TOO_LARGE400File exceeds 25MB limitCompress or split document
INVALID_BASE64400Malformed base64 encodingCheck encoding
CORRUPTED_DOCUMENT422PDF cannot be processedRe-export PDF

Recipient Errors

Error CodeHTTP StatusCauseSolution
RECIPIENT_NOT_FOUND404Recipient ID doesn't existVerify recipient ID
INVALID_EMAIL400Email format invalidUse valid email address
DUPLICATE_RECIPIENT400Same email added twiceUse unique emails or clientUserId
RECIPIENT_ALREADY_SIGNED409Recipient has completed signingCannot modify after signing

View/Signing URL Errors

Error CodeHTTP StatusCauseSolution
VIEW_URL_EXPIRED400Signing URL expired (5 min TTL)Generate new URL
RECIPIENT_NOT_EMBEDDED400No clientUserId setAdd clientUserId to recipient
INVALID_RETURN_URL400returnUrl is malformedUse valid HTTPS URL

Rate Limiting Errors

Error CodeHTTP StatusCauseSolution
RATE_LIMIT_EXCEEDED429Too many requestsImplement backoff and retry

Handling Errors in Code

Basic Error Handler

response=$(curl -s -w "\n%{http_code}" -X POST \
"https://api.propper.ai/restapi/v2.1/accounts/$ACCOUNT_ID/envelopes" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"emailSubject":"Test"}')

http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')

if [ "$http_code" -ge 400 ]; then
error_code=$(echo "$body" | jq -r '.errorCode')
message=$(echo "$body" | jq -r '.message')
echo "Error $http_code: $error_code - $message"
exit 1
fi

echo "Success: $body"

Retry Strategies

Exponential Backoff

For transient errors (429, 503, 500), implement exponential backoff:

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

const withRetry = async (fn, maxRetries = 3, baseDelay = 1000) => {
let lastError;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;

// Only retry on transient errors
const retryable = [429, 500, 502, 503, 504];
if (!retryable.includes(error.status)) {
throw error;
}

// Check for Retry-After header
const retryAfter = error.retryAfter || (baseDelay * Math.pow(2, attempt));

console.log(`Attempt ${attempt + 1} failed, retrying in ${retryAfter}ms...`);
await sleep(retryAfter);
}
}

throw lastError;
};

// Usage
const envelope = await withRetry(() =>
apiRequest(`${BASE_URL}/accounts/${accountId}/envelopes/${envelopeId}`)
);

Rate Limiting Best Practices

  1. Check Retry-After header - Use server-suggested delay
  2. Implement request queuing - Don't burst requests
  3. Cache responses - Reduce redundant calls
  4. Use webhooks - Instead of polling for status
// Rate limiter with queue
class RateLimiter {
constructor(requestsPerSecond = 10) {
this.interval = 1000 / requestsPerSecond;
this.lastRequest = 0;
this.queue = [];
this.processing = false;
}

async request(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this.processQueue();
});
}

async processQueue() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;

while (this.queue.length > 0) {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequest;

if (timeSinceLastRequest < this.interval) {
await sleep(this.interval - timeSinceLastRequest);
}

const { fn, resolve, reject } = this.queue.shift();
this.lastRequest = Date.now();

try {
resolve(await fn());
} catch (error) {
reject(error);
}
}

this.processing = false;
}
}

// Usage
const limiter = new RateLimiter(10); // 10 requests per second

const envelopes = await Promise.all(
envelopeIds.map(id =>
limiter.request(() => apiRequest(`${BASE_URL}/envelopes/${id}`))
)
);

Debugging Checklist

When encountering errors, check these common issues:

Authentication Issues

  • Is the token included in the Authorization header?
  • Is the token format correct? (Bearer <token>)
  • Has the token expired? (Default: 1 hour)
  • Are you using the correct base URL? (https://api.propper.ai)
  • Does the token have the required scopes?

Request Issues

  • Is the Content-Type header set to application/json?
  • Is the request body valid JSON?
  • Are all required fields present?
  • Are IDs in the correct format (UUIDs)?
  • Is the account ID correct?

Envelope Issues

  • Does the envelope exist? (Check with GET first)
  • Is the envelope in the correct state for this operation?
  • Are there documents attached?
  • Are there recipients defined?
  • For embedded signing, is clientUserId set?

Common Mistakes and Fixes

Mistake 1: Using Wrong Account ID

// Wrong - using DocuSign account format
const accountId = '12345678';

// Correct - using Propper organization UUID
const accountId = '550e8400-e29b-41d4-a716-446655440000';

Mistake 2: Missing clientUserId for Embedded Signing

// Wrong - no clientUserId, can't get signing URL
const recipients = {
signers: [{
email: 'signer@example.com',
name: 'John',
recipientId: '1',
}],
};

// Correct - clientUserId enables embedded signing
const recipients = {
signers: [{
email: 'signer@example.com',
name: 'John',
recipientId: '1',
clientUserId: 'your-user-id-123', // Required for embedded
}],
};

Mistake 3: Not Handling Token Expiry

// Wrong - token expires, all requests fail
const accessToken = await getToken();
// ... hours later ...
await apiRequest(url); // 401 Unauthorized

// Correct - check expiry and refresh
const getValidToken = async () => {
if (!tokenCache || tokenCache.expiresAt < Date.now()) {
tokenCache = await refreshToken();
}
return tokenCache.token;
};

Mistake 4: Modifying Sent Envelopes

// Wrong - can't add documents to sent envelope
await apiRequest(`${url}/envelopes/${envelopeId}/documents`, {
method: 'POST',
body: JSON.stringify(newDocument),
});
// Error: ENVELOPE_CANNOT_BE_MODIFIED

// Correct - check status first, void if needed
const envelope = await apiRequest(`${url}/envelopes/${envelopeId}`);
if (envelope.status !== 'created') {
// Void and create new envelope, or use correction workflow
}

Mistake 5: Polling Instead of Webhooks

// Wrong - polling wastes API calls
const waitForCompletion = async (envelopeId) => {
while (true) {
const envelope = await apiRequest(`${url}/envelopes/${envelopeId}`);
if (envelope.status === 'completed') return envelope;
await sleep(5000); // Wastes ~720 API calls per hour
}
};

// Correct - use webhooks
app.post('/webhooks/sign', (req, res) => {
if (req.body.type === 'document.completed') {
processCompletedEnvelope(req.body.data.document.id);
}
res.status(200).send();
});

Logging Best Practices

Log enough context for debugging without exposing sensitive data:

const logger = {
info: (msg, context) => console.log(JSON.stringify({ level: 'info', msg, ...context })),
error: (msg, context) => console.error(JSON.stringify({ level: 'error', msg, ...context })),
};

const apiRequest = async (url, options) => {
const requestId = crypto.randomUUID();

logger.info('API request started', {
requestId,
method: options.method || 'GET',
url: url.replace(/accounts\/[^/]+/, 'accounts/***'), // Mask account ID
});

try {
const response = await fetch(url, options);
const data = await response.json();

if (!response.ok) {
logger.error('API request failed', {
requestId,
status: response.status,
errorCode: data.errorCode,
// Don't log full message - may contain PII
propperRequestId: data.requestId,
});
throw new PropperApiError(response.status, data.errorCode, data.message, data.requestId);
}

logger.info('API request succeeded', {
requestId,
status: response.status,
// Log only IDs, not full response
envelopeId: data.envelopeId,
});

return data;
} catch (error) {
logger.error('API request error', {
requestId,
error: error.message,
// Include request ID for support
propperRequestId: error.requestId,
});
throw error;
}
};

Getting Support

When contacting support, include:

  1. Request ID - From the error response
  2. Timestamp - When the error occurred (UTC)
  3. Endpoint - Full URL path (mask sensitive data)
  4. HTTP Status - Status code received
  5. Error Code - The errorCode from response
  6. Steps to reproduce - What you were trying to do
Subject: API Error - ENVELOPE_NOT_FOUND

Request ID: req_xyz789abc123
Timestamp: 2024-01-15T10:30:00Z
Endpoint: POST /restapi/v2.1/accounts/***/envelopes/***/views/recipient
HTTP Status: 404
Error Code: ENVELOPE_NOT_FOUND

Steps:
1. Created envelope with POST /envelopes (success, got ID abc123)
2. Immediately called POST /envelopes/abc123/views/recipient
3. Got 404 ENVELOPE_NOT_FOUND

Expected: Signing URL returned
Actual: 404 error

Next Steps