API Best Practices

Production-ready patterns for error handling, performance optimization, and monitoring.

Error Handling Patterns

The API uses standard HTTP status codes. Handle these codes in your integration for a robust experience.

Status Meaning Action
200 Success (sync conversion complete) Download the result from download_url
202 Accepted (async processing started) Poll status_url until status is done
401 Unauthorized โ€” invalid or missing token Check your Authorization header. Generate a new token if needed.
402 Insufficient credits Top up credits. Response includes credits_needed and credits_available.
404 Workflow or execution not found Check workflow_id via GET /api/v2/user/workflows
422 Validation error Check the errors object for field-specific messages
429 Rate limit exceeded Wait retry_after seconds, then retry
503 Conversion service temporarily unavailable Wait retry_after seconds. The service will recover automatically.

PHP error handling example

<?php
$ch = curl_init('https://pdfen.com/api/v2/convert');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer YOUR_API_TOKEN'],
    CURLOPT_POSTFIELDS => [
        'files[]' => new CURLFile('document.docx'),
        'workflow_id' => 1,
        'async' => 'false',
    ],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$result = json_decode($response, true);

switch ($httpCode) {
    case 200:
    case 202:
        // Success โ€” process the result
        break;
    case 401:
        throw new Exception('Invalid API token');
    case 402:
        throw new Exception("Insufficient credits: need {$result['credits_needed']}, have {$result['credits_available']}");
    case 422:
        $errors = implode(', ', array_map('implode', $result['errors'] ?? []));
        throw new Exception("Validation failed: $errors");
    case 429:
        // Rate limited โ€” wait and retry
        sleep($result['retry_after'] ?? 60);
        break;
    case 503:
        // Service temporarily down โ€” wait and retry
        sleep($result['retry_after'] ?? 300);
        break;
    default:
        throw new Exception("API error ($httpCode): " . ($result['error'] ?? 'Unknown error'));
}

Retry Logic

For production applications, implement exponential backoff to handle transient errors gracefully. Only retry on 429, 503, and network errors โ€” never on 401, 402, or 422.

PHP โ€” Exponential backoff

<?php
function apiRequestWithRetry(string $url, array $options, int $maxRetries = 3): array
{
    $attempt = 0;

    while ($attempt <= $maxRetries) {
        $ch = curl_init($url);
        curl_setopt_array($ch, $options);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        // Network error
        if ($curlError) {
            $attempt++;
            if ($attempt > $maxRetries) {
                throw new Exception("Network error after $maxRetries retries: $curlError");
            }
            sleep(pow(2, $attempt)); // 2, 4, 8 seconds
            continue;
        }

        $result = json_decode($response, true);

        // Retryable status codes
        if (in_array($httpCode, [429, 503])) {
            $waitSeconds = $result['retry_after'] ?? pow(2, $attempt + 1);
            $attempt++;
            if ($attempt > $maxRetries) {
                throw new Exception("API returned $httpCode after $maxRetries retries");
            }
            sleep($waitSeconds);
            continue;
        }

        // Non-retryable errors
        if ($httpCode >= 400) {
            throw new Exception("API error ($httpCode): " . ($result['error'] ?? $result['message'] ?? 'Unknown'));
        }

        return $result;
    }

    throw new Exception('Max retries exceeded');
}

Do not retry 4xx errors (except 429)

Retrying 401, 402, or 422 will always fail โ€” fix the issue first (invalid token, insufficient credits, invalid parameters).

Performance Tips

Choose the right processing strategy for your use case.

Async vs sync

Mode Best for Behavior
async=false Small files, single documents, quick conversions Request blocks until conversion completes. Returns 200 with download URL.
async=true Large files, batch processing, long conversions Returns 202 immediately. Poll status_url or use webhooks.

Webhooks vs polling

For async conversions, you can either poll the status endpoint or register a webhook to receive notifications.

Polling

  • + Simple to implement
  • + No public endpoint needed
  • - Adds latency between checks
  • - Uses more API calls

Webhooks (recommended)

  • + Instant notifications
  • + No wasted API calls
  • - Requires public HTTPS endpoint
  • - Needs signature verification

General tips

  • Batch files: Upload multiple files in a single request instead of making separate calls per file.
  • Use sync for small jobs: If your file is under 5 MB and only one file, async=false avoids polling overhead.
  • Reuse tokens: Create one token and reuse it โ€” don't create a new token per request.
  • Check options upfront: Call GET /api/v2/workflows/{'{id}'}/options once to discover available options, not on every request.

Monitoring & Rate Limits

Monitor your API usage and credit balance to avoid interruptions.

Rate limit headers

Rate limited responses (429) include a retry_after field indicating how many seconds to wait before retrying.

// 429 response body
{
  "error": "Rate limit exceeded",
  "retry_after": 60
}

Credit balance monitoring

Check your credit balance regularly to avoid failed conversions. The credits endpoint shows both personal and organization credits.

curl https://pdfen.com/api/v2/user/credits \
  -H "Authorization: Bearer YOUR_API_TOKEN"

PHP โ€” Low credit warning

<?php
function checkCredits(string $token, int $threshold = 10): void
{
    $ch = curl_init('https://pdfen.com/api/v2/user/credits');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
    ]);
    $result = json_decode(curl_exec($ch), true);
    curl_close($ch);

    $available = $result['total_available'] ?? 0;

    if ($available < $threshold) {
        // Trigger your alerting system
        error_log("PDFen credit balance low: $available credits remaining");
    }
}

Pre-conversion credit check

Each conversion response includes credits_charged and credits_remaining. Use these to track usage without extra API calls.

// Successful conversion response
{
  "execution_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "done",
  "credits_charged": 3,
  "credits_remaining": 42,
  "download_url": "https://pdfen.com/api/v2/executions/550e8400.../download"
}