Skip to main content

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"
}
FieldDescription
statusCodeHTTP status code
codeMachine-readable error code (e.g. PLAN_PAGE_LIMIT_EXCEEDED). Branch on this
messageHuman-readable explanation. English by default, Japanese with Accept-Language: ja. Validation errors only: an array of strings
errorHTTP reason phrase (Bad Request, …)
detailsPer-field details (field / expected / code / message). Empty array when there are none
docsUrlURL of the explanation for this code on Error Codes
retryAfterOnly 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​

CodeMeaningNotes
200 OKSuccessThe request was processed normally
202 AcceptedAcceptedAsynchronous 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 (appkey header) 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 requestId is 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 designId or version that does not exist returns DESIGN_VERSION_NOT_FOUND. Check the designId + version pair (a template ID is a 16-character alphanumeric string such as 0eUDdgAjNXrrItA2; its format is not checked, so a wrong ID also returns this 404 rather than a 400)
  • Only a parameter fetch without version returns DESIGN_NOT_FOUND for a missing template (with version it is DESIGN_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 409 FILE_NOT_READY (a request whose job record could not be saved returns 404 while still generating), a failed generation returns 422 FILE_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-After header (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 appkey header or an Authorization: Bearer <token> header is set on the request
Why 412 and not 401?

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.code of 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-After and 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

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​

codeCauseFix
DESIGN_VERSION_NOT_FOUNDOn PDF generation or a parameter fetch with version, the template ID or the version does not existCopy the template ID (a 16-character alphanumeric string such as 0eUDdgAjNXrrItA2) from the dashboard again and check version
DESIGN_NOT_FOUNDOn a parameter fetch without version, the template ID does not existCopy the template ID from the dashboard again
FILE_NOT_FOUNDDownload: wrong ID, another workspace's ID, a single generation that failed more than 24 hours ago, or deleted outputUse the values from the generation response and the credentials of the workspace that generated them
FILE_NAME_INVALIDThe filename contains / \ : * ? " < > | or a control characterRemove or replace those characters
INVALID_CREDENTIALSAuthentication failedVerify the appkey value (or access token)
AUTH_HEADER_MISSINGNo authentication headerAdd appkey: <key> or Authorization: Bearer <token> to the request
FILE_NOT_READYYou tried to download a file that is still generatingWait Retry-After seconds, or wait for completion with Get Generation Status

See Error Codes for every code.

Next steps​