Error Handling
Errors from the PDF generation and download endpoints come back as JSON with statusCode, code, message, error, details and docsUrl (the OAuth token endpoint is the exception — it uses the RFC 6749 format, see OAuth 2.0). When your code needs to branch, use code, not the message text. A 4xx other than 429 and 409 (FILE_NOT_READY) means the request itself is wrong and retrying unchanged will fail again (one exception: a 404 from the status or download API right after an async generation may still be in progress, so re-check it once or twice). A 500, 502 or 503 is worth retrying with exponential backoff (a 504 / 524 on a sync generation is not: it may still be running and would be counted twice; other 5xx codes are not resent automatically either, since the request may already have reached the server), while a 429 or 409 can be resent unchanged after waiting the number of seconds the server returns in Retry-After. This guide lists the causes behind each status code and shows how to implement retries, timeouts, and logging.
What does an error response look like?
All errors are returned in the following form:
{
"statusCode": 403,
"code": "PLAN_PAGE_LIMIT_EXCEEDED",
"message": "Monthly page limit reached (30 pages). Upgrade your plan or wait until next month.",
"error": "Forbidden",
"details": [],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#plan_page_limit_exceeded"
}
| Field | Description |
|---|---|
statusCode | HTTP status code |
code | Machine-readable error code (e.g. PLAN_PAGE_LIMIT_EXCEEDED). Branch on this |
message | Human-readable explanation. English by default, Japanese with Accept-Language: ja. Validation errors only: an array of strings |
error | HTTP reason phrase (Bad Request, …) |
details | Per-field details (field / expected / code / message). Empty array when there are none |
docsUrl | URL of the explanation for this code on Error Codes |
retryAfter | Only on 429 and 409 FILE_NOT_READY: seconds to wait before retrying (same value as the Retry-After header) |
The full list of codes, language selection, and when message is an array are covered in Error Codes.
HTTP status codes
2xx Success
| Code | Meaning | Notes |
|---|---|---|
| 200 OK | Success | The request was processed normally |
| 202 Accepted | Accepted | Asynchronous processing was accepted |
4xx Client errors
400 Bad Request
Cause: The request shape or content is invalid.
{
"statusCode": 400,
"code": "VALIDATION_FAILED",
"message": [
"version must be an integer number"
],
"error": "Bad Request",
"details": [
{ "field": "version", "message": "version must be an integer number" }
],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#validation_failed"
}
What to do:
- Check the field pointed to by
details[].field - Check types (integer, …)
- Check required fields
Example: parameter type mismatch (PARAMS_TYPE_MISMATCH)
When a params value does not match the template's type, details carries the key and the expected type.
{
"statusCode": 400,
"code": "PARAMS_TYPE_MISMATCH",
"message": "Parameter validation failed: \"amount\" must be a number",
"error": "Bad Request",
"details": [
{
"field": "params.amount",
"expected": "number",
"code": "PARAMS_TYPE_MISMATCH",
"message": "\"amount\" must be a number"
}
],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#params_type_mismatch"
}
Read the key from details[].field (params.<key>) and the expected type from details[].expected (string / number / date / boolean / object / array).
Forbidden characters in a file name (FILE_NAME_INVALID), duplicate file names (FILE_NAME_DUPLICATED) and similar problems also return 400. See Error Codes for what to do for each code.
401 Unauthorized
Cause: Invalid credentials.
{
"statusCode": 401,
"code": "INVALID_CREDENTIALS",
"message": "Invalid API key or access token.",
"error": "Unauthorized",
"details": [],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#invalid_credentials"
}
What to do:
- Check the application key (
appkeyheader) or access token - Check whether the key was regenerated, and the token's expiry and scope (
pdf:generate)
403 Forbidden
Cause: Plan limit exceeded or insufficient permission.
{
"statusCode": 403,
"code": "PLAN_PAGE_LIMIT_EXCEEDED",
"message": "Monthly page limit reached (30 pages). Upgrade your plan or wait until next month.",
"error": "Forbidden",
"details": [],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#plan_page_limit_exceeded"
}
What to do:
PLAN_PAGE_LIMIT_EXCEEDED: the monthly PDF output page limit was reached. Upgrade the plan or wait until next month (the limit counts total output pages, not the number of requests)PLAN_NOT_FOUND: check the workspace plan (Workspace settings → Plan in the dashboard)WORKSPACE_ROLE_DENIED: the user who authorized via OAuth has no permission in the workspace. Ask an admin to check the user's role- Downloading or checking the status of another workspace's
requestIdis not a 403: it returns 404 so that its existence is not revealed (see below)
404 Not Found
Cause: Resource not found.
{
"statusCode": 404,
"code": "DESIGN_VERSION_NOT_FOUND",
"message": "The specified design and version do not exist.",
"error": "Not Found",
"details": [],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#design_version_not_found"
}
What to do:
- On PDF generation, a
designIdorversionthat does not exist returnsDESIGN_VERSION_NOT_FOUND. Check thedesignId+versionpair (a template ID is a 16-character alphanumeric string such as0eUDdgAjNXrrItA2; its format is not checked, so a wrong ID also returns this 404 rather than a 400) - Only a parameter fetch without
versionreturnsDESIGN_NOT_FOUNDfor a missing template (withversionit isDESIGN_VERSION_NOT_FOUND) - A 404 from a download endpoint (
FILE_NOT_FOUND) means the ID is wrong, belongs to another workspace, is a single generation that failed more than 24 hours ago, or the output was deleted. A file that is still generating normally returns 409FILE_NOT_READY(a request whose job record could not be saved returns 404 while still generating), a failed generation returns 422FILE_GENERATION_FAILED
409 Conflict
Cause: The file you are downloading is still being generated (FILE_NOT_READY).
{
"statusCode": 409,
"code": "FILE_NOT_READY",
"message": "The file is still being generated. Retry after 2 seconds or poll GET /v1/file/status/kZ3mP9qL2xV7nR4t.",
"error": "Conflict",
"details": [],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#file_not_ready",
"retryAfter": 2
}
What to do:
- Wait for the
Retry-Afterheader (seconds) and re-send - To wait for completion, use Get Generation Status
412 Precondition Failed
Cause: No authentication header (neither appkey nor Authorization), or a malformed appkey header.
{
"statusCode": 412,
"code": "AUTH_HEADER_MISSING",
"message": "Authentication header is missing. Send an `appkey` header or `Authorization: Bearer <token>`.",
"error": "Precondition Failed",
"details": [],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#auth_header_missing"
}
What to do:
- Confirm the
appkeyheader or anAuthorization: Bearer <token>header is set on the request
A missing authentication header keeps returning 412 for compatibility with existing clients (it was not changed to 401). Use code (AUTH_HEADER_MISSING) to tell it apart.
413 Payload Too Large
Cause: Request body exceeds 50MB.
The response body in this case may not follow the error shape described on this page. Detect it by the 413 status code.
What to do:
- Optimise images (resolution / compression)
- Split the request
- See Limitations
422 Unprocessable Entity
Cause: Generation of the request you are downloading failed (FILE_GENERATION_FAILED).
What to do:
- Check the failure reason in
error.codeof Get Generation Status - Fix the cause (parameter types, …) and generate again as a new request
429 Too Many Requests
Cause: Rate limit exceeded (applied per workspace).
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
"statusCode": 429,
"code": "RATE_LIMITED",
"message": "Rate limit exceeded for this workspace. Please try again in 60 seconds.",
"error": "Too Many Requests",
"details": [],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#rate_limited",
"retryAfter": 60
}
Retry-After header: Per RFC 9110 §10.2.3, the integer number of seconds to wait before retrying. The body's retryAfter holds the same value. Clients should rely on this value rather than parsing the message.
What to do:
- Wait for the duration in
Retry-Afterand re-send - Sync endpoints: 30 req/min; async endpoints: 100 req/min; status API: 300 req/min
- Use the async endpoints for batch processing
- See Limitations
5xx Server errors
500 Internal Server Error
Cause: Internal server error.
{
"statusCode": 500,
"code": "INTERNAL_ERROR",
"message": "Internal Server Error",
"error": "Internal Server Error",
"details": [],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#internal_error"
}
The message of a 5xx is a generic text that does not expose internal details.
What to do:
- Implement retry with backoff
- If it persists, contact support
503 Service Unavailable
Cause: The service temporarily cannot process the request, for example because the PDF generation queue is full (RENDER_QUEUE_FULL).
What to do:
- Retry after a while
Recommended client patterns
1. Catching errors
async function generatePDF(params) {
try {
const response = await axios.post(url, data, config);
return response.data;
} catch (error) {
if (error.response) {
// The server returned an error response. Keep it on the rethrown error
// (the retry logic below reads its status, code and Retry-After)
try {
handleApiError(error.response);
} catch (apiError) {
apiError.response = error.response;
throw apiError;
}
} else if (error.request) {
// No response came back (the connection failed or timed out)
console.error('Network error:', error.message);
// Keep the original code (ECONNREFUSED / ECONNABORTED, ...); the retry check below reads it
const networkError = new Error('Cannot reach the server');
networkError.code = error.code;
throw networkError;
} else {
// Error while building the request
console.error('Request error:', error.message);
throw error;
}
}
}
// When PDFs / ZIPs are requested with responseType: 'arraybuffer', error bodies arrive as binary too.
// Decode them before reading code (413 / 504 / 524 bodies may not be JSON; then this returns null)
function errorBody(data) {
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
try {
return JSON.parse(new TextDecoder().decode(data));
} catch {
return null;
}
}
return data && typeof data === 'object' ? data : null;
}
function handleApiError(response) {
const { status } = response;
const data = errorBody(response.data) ?? {};
// Branch on code, not on message (message depends on the language)
switch (data.code) {
case 'VALIDATION_FAILED':
case 'PARAMS_TYPE_MISMATCH':
console.error('Validation error:', data.details);
throw new Error(`Invalid request: ${data.details.map(d => d.field).join(', ')}`);
case 'AUTH_HEADER_MISSING':
case 'INVALID_CREDENTIALS':
throw new Error('Check your API key (or access token)');
case 'PLAN_PAGE_LIMIT_EXCEEDED':
throw new Error('The monthly page limit has been reached');
case 'DESIGN_NOT_FOUND':
case 'DESIGN_VERSION_NOT_FOUND':
throw new Error('The template or version does not exist');
default:
if (status === 504 || status === 524) {
// Generation may still be running; a resend can generate and count pages twice
throw new Error('Timed out. Generation may still be running, so do not resend; use the async endpoints for long-running generation');
}
if ([500, 502, 503].includes(status)) {
throw new Error('Temporary error. Please retry shortly');
}
console.error('Unexpected error:', data);
throw new Error(`Error (${status} ${data.code}): ${data.message}`);
}
}
2. Retry logic
Retry transient errors (500, 502, 503, 429 and 409 FILE_NOT_READY). Do not resend a sync generation after a 504 / 524 or a client-side timeout: generation may still be running on the server, so a resend can generate and count pages twice. When Retry-After is present, it takes precedence.
async function generatePDFWithRetry(params, options = {}) {
const {
maxRetries = 3,
initialDelay = 1000,
maxDelay = 10000,
backoffFactor = 2
} = options;
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await generatePDF(params);
} catch (error) {
lastError = error;
// Is the error retryable?
if (!isRetryableError(error)) {
throw error;
}
// Do not retry after the last attempt
if (attempt === maxRetries - 1) {
break;
}
// Prefer Retry-After (seconds); otherwise exponential backoff
const retryAfter = Number(error.response?.headers?.['retry-after']);
const delay = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: Math.min(initialDelay * Math.pow(backoffFactor, attempt), maxDelay);
console.log(`Retry ${attempt + 1}/${maxRetries} (in ${delay}ms)`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// Errors that guarantee the request never reached the server
const NOT_SENT_CODES = new Set(['ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN']);
function isRetryableError(error) {
if (!error.response) {
// Retry only errors from before the connection. Do not resend after a timeout
// (ECONNABORTED / ETIMEDOUT): generation may still run on the server and count pages
return NOT_SENT_CODES.has(error.code);
}
const { status } = error.response;
// Retry only 500, 502, 503, 429 and "still generating" (409 FILE_NOT_READY). Check status so this works even when the body is binary.
// Do not resend 504 / 524 (a timeout on the path): generation may continue, and a resend can generate and count pages twice.
// Use the async endpoints for long jobs
return [500, 502, 503, 429, 409].includes(status);
}
3. Timeouts
async function generatePDFWithTimeout(params, timeout = 70000) {
return Promise.race([
generatePDF(params),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}
4. Error logging
function logError(error, context) {
// Decode with errorBody so code is readable even from a binary body (keep a non-JSON string as is)
const raw = error.response?.data;
const body = errorBody(raw) ?? (typeof raw === 'string' ? raw : null);
const errorLog = {
timestamp: new Date().toISOString(),
context,
error: {
message: error.message,
stack: error.stack,
response: error.response ? {
status: error.response.status,
code: body?.code,
data: body
} : null
}
};
console.error(JSON.stringify(errorLog));
// Send to an error tracking service (e.g. Sentry)
// Sentry.captureException(error, { contexts: { custom: context } });
}
// Usage
try {
await generatePDF(params);
} catch (error) {
logError(error, {
operation: 'generatePDF',
designId: params.designId,
fileName: params.fileName
});
throw error;
}
Python example
import requests
import time
import urllib3
from typing import Optional, Dict, Any
class ReportFlowError(Exception):
"""Base class for Re:port Flow API errors"""
def __init__(self, status_code: int, code: str, message, details=None):
self.status_code = status_code
self.code = code
self.message = message # a list for validation errors
self.details = details or []
super().__init__(f"{code} ({status_code}): {message}")
class ValidationError(ReportFlowError):
"""Validation error (400)"""
pass
class AuthenticationError(ReportFlowError):
"""Authentication error (401 / 412)"""
pass
class NotFoundError(ReportFlowError):
"""Resource not found (404)"""
pass
class RateLimitError(ReportFlowError):
"""Rate limited (429)"""
def __init__(self, status_code, code, message, details=None, retry_after=None):
super().__init__(status_code, code, message, details)
self.retry_after = retry_after
class ServerError(ReportFlowError):
"""Server error (500 / 502 / 503)"""
pass
class SyncTimeoutError(ReportFlowError):
"""Sync generation timed out (504 / 524 / client side).
Generation may still be running and a resend can count pages twice, so it is not retried. Use async for long jobs"""
pass
class ConnectionLostError(SyncTimeoutError):
"""The connection may have dropped after the request was sent (outcome unknown). Not retried, for the same reason"""
pass
def generate_pdf_with_retry(params: Dict[str, Any], max_retries: int = 3) -> bytes:
"""PDF generation with retries (only 429 and 500 / 502 / 503 are resent)"""
for attempt in range(max_retries):
try:
return generate_pdf(params)
except RateLimitError as e:
if attempt == max_retries - 1:
raise
time.sleep(e.retry_after or 60)
except ServerError:
if attempt == max_retries - 1:
raise
delay = 1000 * (2 ** attempt) # exponential backoff
time.sleep(delay / 1000)
print(f"Retry {attempt + 1}/{max_retries}")
except ReportFlowError:
# Do not retry other errors (including SyncTimeoutError)
raise
def generate_pdf(params: Dict[str, Any]) -> bytes:
"""PDF generation"""
url = "https://api.re-port-flow.com/v1/file/sync/single"
headers = {
'appkey': params['app_key'],
'Content-Type': 'application/json'
}
data = {
'designId': params['design_id'],
'version': params['version'],
'content': {
'fileName': params['file_name'],
'params': params['data']
}
}
try:
response = requests.post(url, headers=headers, json=data, timeout=70)
response.raise_for_status()
return response.content
except requests.exceptions.HTTPError as e:
handle_http_error(e.response)
except requests.exceptions.ConnectTimeout:
# Timed out before the connection was established; the request never arrived, so resending is safe
raise ServerError(503, 'CONNECT_TIMEOUT', 'Could not connect')
except requests.exceptions.Timeout:
raise SyncTimeoutError(504, 'TIMEOUT', 'The request timed out')
except requests.exceptions.ConnectionError as e:
reason = getattr(e.args[0], 'reason', None) if e.args else None
if isinstance(reason, urllib3.exceptions.NewConnectionError):
# Connection refused / name resolution failed (includes NameResolutionError); never sent, so resending is safe
raise ServerError(503, 'CONNECT_FAILED', str(e))
# May have dropped after sending (RemoteDisconnected, ...); do not resend
raise ConnectionLostError(0, 'CONNECTION_LOST', str(e))
except requests.exceptions.RequestException as e:
# A drop while receiving (ChunkedEncodingError, ...) leaves the outcome unknown; do not resend
raise ConnectionLostError(0, 'CONNECTION_LOST', str(e))
def handle_http_error(response):
"""Convert an HTTP error into the matching exception"""
try:
error_data = response.json()
status = error_data.get('statusCode', response.status_code)
code = error_data.get('code', 'UNKNOWN')
message = error_data.get('message', 'Unknown error')
details = error_data.get('details', [])
except ValueError:
status = response.status_code
code = 'UNKNOWN'
message = response.text
details = []
if status == 400:
raise ValidationError(status, code, message, details)
elif status in (401, 412):
raise AuthenticationError(status, code, message, details)
elif status == 404:
raise NotFoundError(status, code, message, details)
elif status == 429:
retry_after = response.headers.get('Retry-After')
raise RateLimitError(status, code, message, details,
int(retry_after) if retry_after else None)
elif status in (504, 524):
raise SyncTimeoutError(status, code, message, details)
elif status in (500, 502, 503):
raise ServerError(status, code, message, details)
else:
raise ReportFlowError(status, code, message, details)
# Usage
try:
pdf_data = generate_pdf_with_retry({
'app_key': 'your-app-key',
'design_id': '0eUDdgAjNXrrItA2',
'version': 1,
'file_name': 'invoice.pdf',
'data': {
'customerName': 'John Doe',
'invoiceNumber': 'INV-2024-001'
}
})
with open('invoice.pdf', 'wb') as f:
f.write(pdf_data)
except ValidationError as e:
print(f"Validation error: {e.details}")
except AuthenticationError as e:
print(f"Authentication error: check your API key ({e.code})")
except NotFoundError as e:
print(f"Not found: {e.code}")
except RateLimitError as e:
print(f"Rate limited; would retry after {e.retry_after}s")
except ServerError:
print("Server error: please retry shortly")
except ReportFlowError as e:
print(f"Error: {e}")
Troubleshooting
Common errors
code | Cause | Fix |
|---|---|---|
DESIGN_VERSION_NOT_FOUND | On PDF generation or a parameter fetch with version, the template ID or the version does not exist | Copy the template ID (a 16-character alphanumeric string such as 0eUDdgAjNXrrItA2) from the dashboard again and check version |
DESIGN_NOT_FOUND | On a parameter fetch without version, the template ID does not exist | Copy the template ID from the dashboard again |
FILE_NOT_FOUND | Download: wrong ID, another workspace's ID, a single generation that failed more than 24 hours ago, or deleted output | Use the values from the generation response and the credentials of the workspace that generated them |
FILE_NAME_INVALID | The filename contains / \ : * ? " < > | or a control character | Remove or replace those characters |
INVALID_CREDENTIALS | Authentication failed | Verify the appkey value (or access token) |
AUTH_HEADER_MISSING | No authentication header | Add appkey: <key> or Authorization: Bearer <token> to the request |
FILE_NOT_READY | You tried to download a file that is still generating | Wait Retry-After seconds, or wait for completion with Get Generation Status |
See Error Codes for every code.