Error Handling
Every error the API returns, and what to do about it.
Error Response Format
All errors share the same shape:
{
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Insufficient credits to generate PDF. Please purchase credits to continue.",
"details": {
"current_balance": 0.005,
"required_amount": 0.01,
"subscription_credits_remaining": 0,
"action_required": "purchase_credits"
}
}
}
Match on error.code, not on message. details varies by code and often carries the values you need to fix the request.
HTTP Status Codes
| Status | Meaning |
|---|---|
| 400 | The request is malformed or a value is invalid |
| 401 | Missing or invalid credentials |
| 402 | The account has no paid plan |
| 403 | Authenticated, but not allowed: allowance or credits, plan features, ownership, rate limits |
| 404 | The job, upload or output does not exist |
| 408 | A quick job exceeded the 30-second limit |
| 409 | The request conflicts with the current state |
| 410 | The output existed but has been deleted |
| 413 | The inline payload is too large |
| 422 | The request was valid but a remote URL could not be used |
| 429 | Platform throttling — retry with backoff |
| 500 | Something failed on our side |
Request Errors (400)
| Code | Meaning | Fix |
|---|---|---|
MISSING_INPUT_TYPE | No input_type | Send html, markdown, image or url |
INVALID_INPUT_TYPE | Unknown input_type | Use a supported value |
MISSING_CONTENT_FIELD | The field matching input_type is absent | Send html, markdown or url |
EMPTY_CONTENT_FIELD | The content field is empty | Send real content |
CONFLICTING_FIELDS | Both html and markdown sent | Send only the one matching input_type |
WRONG_FIELD_PROVIDED | A field does not match input_type | Remove it |
CONTENT_TYPE_MISMATCH | Content looks like a different type | Correct input_type |
INPUT_SIZE_EXCEEDED | Content over 5 MB | Split the document |
PAGE_LIMIT_EXCEEDED | The PDF exceeds the page limit | Use /longjob, or split the document |
INVALID_PARAMETER | A parameter is invalid | See details.parameter |
INVALID_OPTIONS_JSON | options is not valid JSON | Fix the JSON |
INVALID_MULTIPART | Malformed multipart request | Check the form encoding |
MISSING_IMAGES | No image files in the request | Attach at least one image |
INVALID_IMAGE_FORMAT | Not a PNG or JPEG | Convert the image |
INVALID_IMAGE_DATA | The image is corrupt | Re-export it |
IMAGE_TOO_LARGE | Over 5 MB or 10,000 × 10,000 pixels | Resize |
MISSING_URL | input_type: url without url | Send the URL |
INVALID_URL | Not HTTPS, malformed, or a private address | Use a public https:// URL |
INVALID_WEBHOOK_URL | Webhook URL is not HTTPS | Use HTTPS |
Bulk-specific request errors are listed in POST /bulkjob.
Authentication and Account (401, 402, 403)
| Status | Code | Meaning | Fix |
|---|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid API key or token | Check the X-API-Key header, or refresh your token |
| 402 | UPGRADE_REQUIRED | The account has no paid plan | Buy a credit pack or start a subscription to activate the account |
| 403 | ACCOUNT_NOT_FOUND | No account record for this user | Create an account first |
| 403 | INSUFFICIENT_CREDITS | Subscription allowance, free credits and one-off credits together cannot cover the work | Buy credits or upgrade your plan; details shows current_balance (one-off credits), subscription_credits_remaining and required_amount |
| 403 | CONVERSION_TYPE_NOT_ENABLED | Your plan does not allow that input type | details.enabled_types lists what is allowed |
| 403 | RATE_LIMIT_EXCEEDED | Your plan's per-minute limit | Wait details.retry_after seconds |
| 403 | ACCESS_DENIED | The resource belongs to someone else | Check the ID |
Rate limiting only applies to plans that define a per-minute limit. Platform-level throttling is separate and returns 429.
Jobs and Output (404, 409, 410)
| Status | Code | Meaning | Fix |
|---|---|---|---|
| 404 | JOB_NOT_FOUND | Unknown job, or it belongs to another account | Check the ID |
| 404 | OUTPUT_NOT_AVAILABLE | The job has no stored output | Quick jobs need store: true |
| 409 | JOB_NOT_READY | The job has not finished | Poll until completed or partial_failed |
| 410 | OUTPUT_EXPIRED | Deleted after 30 days | Convert again |
Timeouts and Remote URLs (408, 422)
| Status | Code | Meaning | Fix |
|---|---|---|---|
| 408 | QUICKJOB_TIMEOUT | Rendering passed 30 seconds | Use /longjob |
| 422 | URL_FETCH_TIMEOUT | The remote server took over 10 seconds | Check the URL |
| 422 | URL_FETCH_HTTP_ERROR | The remote server returned an error | details.status_code shows what it returned |
| 422 | URL_CONTENT_TYPE_NOT_SUPPORTED | The URL returned something other than HTML, Markdown, PNG or JPEG | Use a supported page |
| 422 | URL_FETCH_FAILED | The URL could not be fetched | Check that it is publicly reachable |
Server Errors (500)
| Code | Meaning | Fix |
|---|---|---|
PDF_GENERATION_FAILED | Rendering failed | Retry; if it persists, check the document for very large assets |
INTERNAL_SERVER_ERROR | Unexpected failure | Retry with backoff |
Handling Errors in Code
const response = await fetch('https://api.podpdf.com/quickjob', {
method: 'POST',
headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify({ input_type: 'html', html }),
});
if (!response.ok) {
const { error } = await response.json();
switch (error.code) {
case 'INSUFFICIENT_CREDITS':
case 'UPGRADE_REQUIRED':
throw new Error('Buy credits or upgrade your plan before converting');
case 'PAGE_LIMIT_EXCEEDED':
return convertWithLongJob(html); // 25-page limit on /quickjob
case 'QUICKJOB_TIMEOUT':
return convertWithLongJob(html);
case 'RATE_LIMIT_EXCEEDED':
await sleep((error.details.retry_after ?? 60) * 1000);
return retry();
default:
throw new Error(`${error.code}: ${error.message}`);
}
}
Retry advice
- Retry with backoff: 429 and 500.
- Do not retry unchanged: every 4xx except 429 — fix the request, the plan or the balance first.
- Switch endpoint:
PAGE_LIMIT_EXCEEDEDorQUICKJOB_TIMEOUTon/quickjobusually means the document belongs on/longjob.
Bulk Jobs
A bulk job can finish with status: "partial_failed" — some files converted, others did not. That is not an HTTP error: check GET /jobs/{job_id}/files for per-file codes, and remember only successful PDFs are billed. See the bulk conversion guide.
Merge and Split
Errors from /pdf/merge, /pdf/split and /pdf/jobs name the file that caused them in details.filename and details.input_index.
- Never retry unchanged:
PDF_ENCRYPTED,INVALID_PDF,PDF_NO_PAGES— the file itself has to change. - Retry later:
PDF_JOB_ALREADY_ACTIVE(409), once one of your running jobs finishes. - Switch path:
PDF_INPUT_TOO_LARGE,PDFOP_TIMEOUTor a413on/pdf/mergeor/pdf/splitmeans the files belong in an upload and a job. - Warnings are not errors:
FORM_FIELDS_NOT_PRESERVED,BOOKMARKS_NOT_PRESERVEDandSIGNATURE_INVALIDATEDcome back with a successful result.