HTTP status codes, the error response shape returned by the API, and patterns for handling errors gracefully in your integration.
HTTP Status Codes
Success Codes
Code Meaning Description 200 OK Request succeeded 201 Created Resource created successfully 204 No Content Request succeeded, no content returned
Client Error Codes
Code Meaning Common Causes 400 Bad Request Invalid JSON, missing required fields, validation errors, unsupported attachment types, malformed PDF 401 Unauthorized Missing, invalid, or expired bearer token 403 Forbidden Access denied or authentication error for the requested operation 404 Not Found Resource does not exist or has been deleted 406 Not Acceptable Content type not supported, or a required attribute is missing 422 Unprocessable Entity PDF document could not be rendered (e.g. for a page preview) 429 Too Many Requests Rate limit exceeded
Server Error Codes
Code Meaning Action 500 Internal Server Error Retry with exponential backoff 503 Service Unavailable Service is temporarily down, retry later
Error Response Format
Both the Sign API and the Validation API share the same error envelope (Problem+JSON-style):
{
"type" : "https://api.skribble.com/problem/problem-with-message" ,
"method" : "POST" ,
"path" : "/v2/signature-requests" ,
"status" : 400 ,
"message" : "Validation failed for JSON object"
}
Field Type Always present Description typeURI string yes URI identifying the problem category methodstring yes HTTP method of the failing request pathstring yes Request URI of the failing request statusinteger yes HTTP status code (mirrors the response status line) keystring no Optional machine-readable error key. Currently only present on PDF loading, malware detection, and unsupported attachment errors messagestring yes Human-readable message
Problem types
type URIUsed for https://api.skribble.com/problem/problem-with-messageGeneric error with a message (default) https://api.skribble.com/problem/entity-not-foundRequested resource does not exist (404) https://api.skribble.com/problem/constraint-violationRequest violates a domain constraint https://api.skribble.com/problem/parameterizedParameterized error https://api.skribble.com/problem/limit-exceededA usage limit was exceeded
/access/login is the only endpoint that does not return Problem JSON — on invalid credentials it returns a text/plain body: Please ensure your username and api-key are correct!
Common Errors and Solutions
Authentication and Authorization
401 Unauthorized — token missing, invalid, or expired
Returned by the security layer before reaching the controller. Access tokens issued by /access/login are valid for ~20 minutes; obtain a new one when this happens.
async function apiCall ( url , options ) {
let response = await fetch (url, {
... options,
headers: { ... options.headers, 'Authorization' : `Bearer ${ await getToken () }` }
});
if (response.status === 401 ) {
await refreshToken ();
response = await fetch (url, {
... options,
headers: { ... options.headers, 'Authorization' : `Bearer ${ await getToken () }` }
});
}
return response;
}
403 Forbidden — access denied
{
"type" : "https://api.skribble.com/problem/problem-with-message" ,
"method" : "GET" ,
"path" : "/v2/signature-requests/4f43614a-de50-bfe1-2df3-4c881fc7840e" ,
"status" : 403 ,
"message" : "Access denied"
}
The caller is authenticated but is not permitted to perform the operation on this resource.
Validation
400 Bad Request — validation failed
{
"type" : "https://api.skribble.com/problem/problem-with-message" ,
"method" : "POST" ,
"path" : "/v2/signature-requests" ,
"status" : 400 ,
"message" : "Validation failed for JSON object"
}
Check the API reference for required fields and value constraints.
400 Bad Request — unsupported attachment type
{
"type" : "https://api.skribble.com/problem/problem-with-message" ,
"method" : "POST" ,
"path" : "/v2/signature-requests/.../attachments" ,
"status" : 400 ,
"key" : "error.attachment.unsupported-type" ,
"message" : "Attachment type is not supported"
}
Attachment errors carry the optional key field — branch on key rather than on message if you need to differentiate cases programmatically.
400 Bad Request — PDF document loading failed
The document is either corrupt, password-protected, or not a valid PDF. The response carries a key field describing the specific failure mode.
Resource state
404 Not Found — entity does not exist
{
"type" : "https://api.skribble.com/problem/entity-not-found" ,
"method" : "GET" ,
"path" : "/v2/signature-requests/00000000-0000-0000-0000-000000000000" ,
"status" : 404 ,
"message" : "SignatureRequest not found"
}
Note the dedicated type URI — checking type === 'https://api.skribble.com/problem/entity-not-found' is the most precise way to distinguish "this entity is gone" from other 404 causes (e.g. an unmapped route).
Content negotiation
406 Not Acceptable
{
"type" : "https://api.skribble.com/problem/problem-with-message" ,
"method" : "POST" ,
"path" : "/v2/signature-requests" ,
"status" : 406 ,
"message" : "Content type not supported"
}
Returned when the request Content-Type is not supported, or when a required attribute is missing. Send JSON bodies with Content-Type: application/json.
422 Unprocessable Entity — PDF rendering failed
Returned by document page-preview endpoints when the PDF cannot be rendered. The PDF may be valid but use features the renderer cannot process. Try a different page, a different scale, or re-upload the document.
Rate limiting
429 Too Many Requests
{
"type" : "https://api.skribble.com/problem/problem-with-message" ,
"method" : "POST" ,
"path" : "/v2/signature-requests" ,
"status" : 429 ,
"message" : "Too many requests"
}
Limits are 600 authenticated requests per 10 minutes, 15 unauthenticated requests per minute. The same status is also returned by /signature-requests/{SR_ID}/remind when the per-request reminder limit is reached.
Throttle client-side rather than relying on retries:
class RateLimitedClient {
constructor () {
this .queue = [];
this .processing = false ;
this .minDelay = 100 ; // ms between requests
}
async request ( url , options ) {
return new Promise (( resolve , reject ) => {
this .queue. push ({ url, options, resolve, reject });
this . processQueue ();
});
}
async processQueue () {
if ( this .processing || this .queue. length === 0 ) return ;
this .processing = true ;
while ( this .queue. length > 0 ) {
const { url , options , resolve , reject } = this .queue. shift ();
try {
const response = await fetch (url, options);
if (response.status === 429 ) {
await sleep ( 60_000 );
this .queue. unshift ({ url, options, resolve, reject });
continue ;
}
resolve (response);
} catch (error) {
reject (error);
}
await sleep ( this .minDelay);
}
this .processing = false ;
}
}
Best Practices
1. Parse the Problem envelope
class ApiError extends Error {
constructor ( problem ) {
super (problem.message);
this .type = problem.type;
this .status = problem.status;
this .method = problem.method;
this .path = problem.path;
this .key = problem.key; // optional
}
isEntityNotFound () {
return this .type === 'https://api.skribble.com/problem/entity-not-found' ;
}
}
async function makeApiCall ( url , options ) {
const response = await fetch (url, options);
if ( ! response.ok) {
// /access/login returns text/plain on 400; everything else returns Problem JSON
const ct = response.headers. get ( 'content-type' ) || '' ;
if (ct. includes ( 'application/json' )) {
throw new ApiError ( await response. json ());
}
throw new ApiError ({
type: 'https://api.skribble.com/problem/problem-with-message' ,
method: options.method ?? 'GET' ,
path: new URL (url).pathname,
status: response.status,
message: await response. text ()
});
}
return response. json ();
}
Branch on type (and key when present) rather than scraping message. Messages are not part of the stable contract.
2. Log errors for debugging
async function apiCallWithLogging ( url , options ) {
const requestId = generateRequestId ();
console. log ( `[${ requestId }] Request: ${ options . method } ${ url }` );
try {
const response = await makeApiCall (url, options);
console. log ( `[${ requestId }] Success` );
return response;
} catch (error) {
console. error ( `[${ requestId }] Error` , {
type: error.type,
status: error.status,
key: error.key,
message: error.message
});
throw error;
}
}
3. Circuit breaker for high-availability systems
class CircuitBreaker {
constructor ( options = {}) {
this .failureThreshold = options.failureThreshold || 5 ;
this .resetTimeout = options.resetTimeout || 30000 ;
this .failures = 0 ;
this .state = 'CLOSED' ;
this .nextRetry = null ;
}
async call ( fn ) {
if ( this .state === 'OPEN' ) {
if (Date. now () > this .nextRetry) {
this .state = 'HALF_OPEN' ;
} else {
throw new Error ( 'Circuit breaker is open' );
}
}
try {
const result = await fn ();
this . onSuccess ();
return result;
} catch (error) {
this . onFailure ();
throw error;
}
}
onSuccess () {
this .failures = 0 ;
this .state = 'CLOSED' ;
}
onFailure () {
this .failures ++ ;
if ( this .failures >= this .failureThreshold) {
this .state = 'OPEN' ;
this .nextRetry = Date. now () + this .resetTimeout;
}
}
}
4. Graceful degradation
async function getSignatureStatus ( requestId ) {
try {
return await api. get ( `/signature-requests/${ requestId }` );
} catch (error) {
if (error instanceof NetworkError || error.status >= 500 ) {
const cached = await cache. get ( `request:${ requestId }` );
if (cached) {
return { ... cached, _stale: true };
}
return { status_overall: 'UNKNOWN' , _error: true };
}
throw error;
}
}
Monitoring and Alerting
Health check endpoint
Use the health endpoint to monitor API availability:
async function checkApiHealth () {
try {
const response = await fetch ( `${ BASE_URL }/management/health` );
const health = await response. json ();
return health.status === 'UP' ;
} catch {
return false ;
}
}
// Periodic health check
setInterval ( async () => {
const isHealthy = await checkApiHealth ();
if ( ! isHealthy) {
alertOps ( 'Skribble API health check failed' );
}
}, 60000 );
Error rate monitoring
Track error rates per status and per problem type to detect issues early:
const errorMetrics = {
total: 0 ,
byStatus: {},
byType: {},
byEndpoint: {}
};
function recordError ( endpoint , status , type ) {
errorMetrics.total ++ ;
errorMetrics.byStatus[status] = (errorMetrics.byStatus[status] || 0 ) + 1 ;
errorMetrics.byType[type] = (errorMetrics.byType[type] || 0 ) + 1 ;
errorMetrics.byEndpoint[endpoint] = (errorMetrics.byEndpoint[endpoint] || 0 ) + 1 ;
}