May 19, 2026

PDF Split API: Split PDF Pages by Range with FUNBREW PDF

PDF APIPDF manipulationsplit PDFdocument processingREST API

Extract a single chapter from a 200-page report. Separate one invoice out of a merged batch PDF. Pull individual certificates out of a bulk-generated archive. These are the tasks the FUNBREW PDF split API was built for.

The split endpoint (POST /api/pdf/split) takes a PDF already stored in your FUNBREW account and extracts the pages you specify — by range, by individual page number, or any combination — returning a new, self-contained PDF file with a timed download URL.

This guide covers the full API specification, the pages parameter format, working code in cURL, Node.js, Python, and PHP, and practical use case patterns.

For generating PDFs before splitting them, see the PDF API Quickstart. For combining multiple PDFs into one, see the PDF Merge API Guide. For reducing file size after splitting, see the PDF Compress API Guide. For the complete overview of PDF manipulation endpoints, see the PDF Manipulation Guide.

How It Works

The split workflow has two steps:

  1. A PDF must already exist in your FUNBREW account (generated by a previous call to POST /api/pdf/generate and still within its expiration window).
  2. You call POST /api/pdf/split with the filename and the pages you want to extract.

The server uses the FPDI library to import the specified pages and write them into a new PDF. The result is stored and returned as a download URL.

The endpoint requires the pdf.feature:split feature gate on your plan (SaaS edition only).

API Specification

POST /api/pdf/split
Content-Type: application/json
Authorization: Bearer {API key}

Request Parameters

Parameter Type Required Description
filename string Yes Filename of an existing PDF in your account
pages string Yes Page specification string (see format below)
expiration_hours integer No Lifetime of the split file in hours (0–168, default 24)
max_downloads integer No Maximum download count before expiry (0–100, default 10)

The pages Parameter Format

The pages string is a comma-separated list of page numbers and ranges. All page numbers are 1-indexed.

Format Example Meaning
Single page "3" Page 3 only
Range "1-5" Pages 1 through 5 inclusive
Multiple pages "1,3,5" Pages 1, 3, and 5
Mixed "1-3,7,10-12" Pages 1–3, page 7, and pages 10–12

The output PDF contains the extracted pages in the order specified. Duplicate page numbers are deduplicated. If any page number exceeds the total page count, the entire request returns a 422 error.

Success Response

{
  "success": true,
  "data": {
    "filename": "split-abc123.pdf",
    "download_url": "https://pdf.funbrew.cloud/api/pdf/download/split-abc123.pdf",
    "file_size": 102400,
    "pages_extracted": 3,
    "expires_at": "2026-05-20T10:00:00Z"
  }
}

pages_extracted is the count of pages in the output file.

Error Responses

Status Reason
422 filename not found in your account, or file belongs to a different API key
422 File has expired and is no longer available
422 File not found on storage (already deleted)
422 Invalid page range — page number exceeds the PDF page count

Code Examples

cURL

Generate a PDF first, then split pages 1–3 from it:

# Step 1: Generate a PDF (capture the returned filename)
RESPONSE=$(curl -s -X POST https://pdf.funbrew.cloud/api/pdf/generate \
  -H "Authorization: Bearer $FUNBREW_PDF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<h1>Chapter 1</h1><p>...</p><div style=\"page-break-after:always\"></div><h1>Chapter 2</h1><p>...</p><div style=\"page-break-after:always\"></div><h1>Chapter 3</h1><p>...</p>",
    "options": { "format": "A4", "responseFormat": "url" }
  }')
FILENAME=$(echo $RESPONSE | jq -r '.data.filename')

# Step 2: Extract pages 1-3
curl -s -X POST https://pdf.funbrew.cloud/api/pdf/split \
  -H "Authorization: Bearer $FUNBREW_PDF_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"filename\": \"$FILENAME\",
    \"pages\": \"1-3\",
    \"expiration_hours\": 48,
    \"max_downloads\": 5
  }" | jq .

Extract individual pages (1, 3, and 5):

curl -s -X POST https://pdf.funbrew.cloud/api/pdf/split \
  -H "Authorization: Bearer $FUNBREW_PDF_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"filename\": \"$FILENAME\",
    \"pages\": \"1,3,5\"
  }" | jq .

Node.js

const API_KEY  = process.env.FUNBREW_PDF_API_KEY;
const BASE_URL = "https://pdf.funbrew.cloud";

/**
 * Generate a PDF and return the server-side filename.
 */
async function generatePdf(html, options = {}) {
  const response = await fetch(`${BASE_URL}/api/pdf/generate`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      html,
      options: { format: "A4", ...options, responseFormat: "url" },
    }),
  });

  if (!response.ok) {
    throw new Error(`PDF generation failed: HTTP ${response.status}`);
  }

  const { data } = await response.json();
  return data.filename;
}

/**
 * Split a stored PDF by page specification.
 *
 * @param {string} filename  - Filename returned from generatePdf()
 * @param {string} pages     - Page spec e.g. "1-3", "1,3,5", "2-5,8"
 * @param {object} options   - { expirationHours, maxDownloads }
 */
async function splitPdf(filename, pages, options = {}) {
  const response = await fetch(`${BASE_URL}/api/pdf/split`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      filename,
      pages,
      expiration_hours: options.expirationHours ?? 24,
      max_downloads:    options.maxDownloads ?? 10,
    }),
  });

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`PDF split failed: HTTP ${response.status} — ${body}`);
  }

  return response.json();
}

// Example: generate a 5-page report, then extract chapters 2–3 (pages 2–3)
async function extractChapter(reportHtml, chapterPages) {
  console.log("Generating report PDF...");
  const filename = await generatePdf(reportHtml);

  console.log(`Extracting pages: ${chapterPages}`);
  const result = await splitPdf(filename, chapterPages, {
    expirationHours: 72,
    maxDownloads: 3,
  });

  console.log("Split complete:");
  console.log("  Pages extracted:", result.data.pages_extracted);
  console.log("  Download URL:  ", result.data.download_url);
  return result.data;
}

// Run
extractChapter(
  "<h1>Chapter 1</h1><p>...</p>",  // your full report HTML
  "2-3"
).catch(console.error);

Python

import os
import httpx
from typing import Optional

API_KEY  = os.environ["FUNBREW_PDF_API_KEY"]
BASE_URL = "https://pdf.funbrew.cloud"


def generate_pdf(html: str, **options) -> str:
    """Generate a PDF and return the server-side filename."""
    response = httpx.post(
        f"{BASE_URL}/api/pdf/generate",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={
            "html": html,
            "options": {"format": "A4", **options, "responseFormat": "url"},
        },
        timeout=120,
    )
    response.raise_for_status()
    return response.json()["data"]["filename"]


def split_pdf(
    filename: str,
    pages: str,
    expiration_hours: int = 24,
    max_downloads: int = 10,
) -> dict:
    """
    Split a stored PDF by page specification.

    Args:
        filename: Filename returned from generate_pdf()
        pages: Page spec string, e.g. '1-3', '1,3,5', '2-5,8,10-12'
        expiration_hours: 0–168
        max_downloads: 0–100
    """
    response = httpx.post(
        f"{BASE_URL}/api/pdf/split",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={
            "filename":         filename,
            "pages":            pages,
            "expiration_hours": expiration_hours,
            "max_downloads":    max_downloads,
        },
        timeout=120,
    )
    response.raise_for_status()
    return response.json()


# Example: extract individual invoice pages from a merged batch PDF
def extract_invoice_pages(batch_filename: str, page_numbers: list[int]) -> str:
    """Extract specific pages from a batch PDF and return a download URL."""
    pages_spec = ",".join(str(p) for p in page_numbers)
    print(f"Extracting pages {pages_spec} from {batch_filename}...")

    result = split_pdf(
        filename=batch_filename,
        pages=pages_spec,
        expiration_hours=48,
        max_downloads=5,
    )

    data = result["data"]
    print(f"Pages extracted: {data['pages_extracted']}")
    print(f"File size:       {data['file_size']} bytes")
    print(f"Download URL:    {data['download_url']}")
    return data["download_url"]


# Generate a batch PDF, then extract pages 1 and 3
if __name__ == "__main__":
    batch_html = """
    <html><body>
      <div style="page-break-after:always"><h1>Invoice #001</h1><p>Acme Corp</p></div>
      <div style="page-break-after:always"><h1>Invoice #002</h1><p>Beta Ltd</p></div>
      <div><h1>Invoice #003</h1><p>Gamma GmbH</p></div>
    </body></html>
    """
    batch_filename = generate_pdf(batch_html)
    download_url = extract_invoice_pages(batch_filename, [1, 3])
    print("Done:", download_url)

PHP

<?php

class FunbrewPdfSplitter
{
    private string $apiKey;
    private string $baseUrl = 'https://pdf.funbrew.cloud';

    public function __construct(string $apiKey)
    {
        $this->apiKey = $apiKey;
    }

    /**
     * Generate a PDF and return the server-side filename.
     */
    public function generatePdf(string $html, array $options = []): string
    {
        $payload = [
            'html'    => $html,
            'options' => array_merge(['format' => 'A4', 'responseFormat' => 'url'], $options),
        ];

        $ch = curl_init("{$this->baseUrl}/api/pdf/generate");
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($payload),
            CURLOPT_HTTPHEADER     => [
                "Authorization: Bearer {$this->apiKey}",
                'Content-Type: application/json',
            ],
            CURLOPT_TIMEOUT        => 120,
        ]);

        $body   = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($status !== 200) {
            throw new \RuntimeException("PDF generation failed: HTTP {$status} — {$body}");
        }

        return json_decode($body, true)['data']['filename'];
    }

    /**
     * Split a stored PDF by page specification.
     *
     * @param string $filename        Filename returned from generatePdf()
     * @param string $pages           Page spec e.g. '1-3', '1,3,5', '2-5,8'
     * @param int    $expirationHours 0–168 (default 24)
     * @param int    $maxDownloads    0–100 (default 10)
     */
    public function split(
        string $filename,
        string $pages,
        int $expirationHours = 24,
        int $maxDownloads = 10
    ): array {
        $payload = [
            'filename'         => $filename,
            'pages'            => $pages,
            'expiration_hours' => $expirationHours,
            'max_downloads'    => $maxDownloads,
        ];

        $ch = curl_init("{$this->baseUrl}/api/pdf/split");
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($payload),
            CURLOPT_HTTPHEADER     => [
                "Authorization: Bearer {$this->apiKey}",
                'Content-Type: application/json',
            ],
            CURLOPT_TIMEOUT        => 120,
        ]);

        $body   = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($status !== 200) {
            throw new \RuntimeException("PDF split failed: HTTP {$status} — {$body}");
        }

        return json_decode($body, true);
    }
}

// Usage (Laravel)
$splitter = new FunbrewPdfSplitter(env('FUNBREW_PDF_API_KEY'));

// Generate a multi-page PDF
$filename = $splitter->generatePdf(
    '<h1>Page 1</h1><div style="page-break-after:always"></div>'
    . '<h1>Page 2</h1><div style="page-break-after:always"></div>'
    . '<h1>Page 3</h1>'
);

// Extract pages 1 and 3
$result = $splitter->split(
    filename:        $filename,
    pages:           '1,3',
    expirationHours: 48,
    maxDownloads:    5
);

echo "Download URL: " . $result['data']['download_url'] . "\n";
echo "Pages extracted: " . $result['data']['pages_extracted'] . "\n";

Use Case Patterns

Large report: chapter-by-chapter distribution

Generate a full report once, then distribute each chapter as a separate PDF — no need to regenerate.

chapters = [
    {"name": "executive-summary", "pages": "1-3"},
    {"name": "financials",        "pages": "4-12"},
    {"name": "appendix",          "pages": "13-20"},
]

# Generate the full report once
report_filename = generate_pdf(full_report_html)

# Split into chapter files
chapter_urls = {}
for chapter in chapters:
    result = split_pdf(
        filename=report_filename,
        pages=chapter["pages"],
        expiration_hours=168,   # 7 days
        max_downloads=50,
    )
    chapter_urls[chapter["name"]] = result["data"]["download_url"]
    print(f"{chapter['name']}: {result['data']['download_url']}")

Invoice batch: isolate a single invoice for re-sending

When a client needs one invoice re-sent from a monthly batch PDF, extract just that page.

async function resendInvoice(batchFilename, invoicePageNumber, recipientEmail) {
  // Extract the single invoice page
  const result = await splitPdf(batchFilename, String(invoicePageNumber), {
    expirationHours: 24,
    maxDownloads: 2,
  });

  const downloadUrl = result.data.download_url;

  // Send download link via your email provider
  await sendEmail({
    to:      recipientEmail,
    subject: "Your invoice (re-sent)",
    body:    `Download your invoice: ${downloadUrl}`,
  });

  console.log(`Invoice page ${invoicePageNumber} sent to ${recipientEmail}`);
  return downloadUrl;
}

Certificate bulk generation: extract individual files

When generating bulk certificates, you may produce a single multi-page PDF containing one certificate per page. Split each page into an individual downloadable file.

import asyncio
import httpx

async def split_certificate_pages(
    batch_filename: str,
    total_pages: int,
    api_key: str,
    concurrency: int = 5,
) -> list[str]:
    """
    Split a bulk certificate PDF into individual certificate files.
    Returns a list of download URLs, one per certificate.
    """
    sem = asyncio.Semaphore(concurrency)

    async def split_one(page_num: int) -> str:
        async with sem:
            async with httpx.AsyncClient() as client:
                response = await client.post(
                    "https://pdf.funbrew.cloud/api/pdf/split",
                    headers={"Authorization": f"Bearer {api_key}"},
                    json={
                        "filename":         batch_filename,
                        "pages":            str(page_num),
                        "expiration_hours": 168,
                        "max_downloads":    3,
                    },
                    timeout=60,
                )
                response.raise_for_status()
                data = response.json()["data"]
                print(f"  Page {page_num}: {data['download_url']}")
                return data["download_url"]

    tasks = [split_one(p) for p in range(1, total_pages + 1)]
    return await asyncio.gather(*tasks)


# After bulk certificate generation, split into individual files
download_urls = asyncio.run(
    split_certificate_pages(
        batch_filename="batch-certificates-abc123.pdf",
        total_pages=50,
        api_key=os.environ["FUNBREW_PDF_API_KEY"],
    )
)
print(f"Generated {len(download_urls)} individual certificate URLs")

Combining Split with Other PDF Operations

The split, merge, and compress endpoints can be chained to build powerful document workflows.

Split then compress

Extract a section of a large PDF and compress the result for email delivery.

# Step 1: Split pages 5-15 from a large PDF
split_result = split_pdf(
    filename="large-report-abc.pdf",
    pages="5-15",
    expiration_hours=2,  # Short-lived intermediate file
)
split_filename = split_result["data"]["filename"]

# Step 2: Compress the extracted section
compress_response = httpx.post(
    f"{BASE_URL}/api/pdf/compress",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "filename": split_filename,
        "quality":  "low",         # Maximum compression for email
        "expiration_hours": 48,
    },
    timeout=60,
)
compress_response.raise_for_status()
final_url = compress_response.json()["data"]["download_url"]
print("Compressed extract ready:", final_url)

Generate, split, then merge

Generate separate documents, split specific sections from each, and merge into a custom deliverable.

// 1. Generate two source PDFs
const [sourceA, sourceB] = await Promise.all([
  generatePdf(contractHtml),
  generatePdf(appendixHtml),
]);

// 2. Extract only the signature pages
const [sigPageA, sigPageB] = await Promise.all([
  splitPdf(sourceA, "5"),      // page 5 of the contract
  splitPdf(sourceB, "1,2"),    // pages 1-2 of the appendix
]);

// 3. Merge the extracted pages into a single signature package
const mergeResponse = await fetch("https://pdf.funbrew.cloud/api/pdf/merge", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    filenames:        [sigPageA.data.filename, sigPageB.data.filename],
    expiration_hours: 168,
    max_downloads:    2,
  }),
});
const mergeResult = await mergeResponse.json();
console.log("Signature package:", mergeResult.data.download_url);

Common Failures and Fixes

File not found (422)

{
  "success": false,
  "message": "File not found or not owned by your company: report-abc.pdf"
}

Root causes:

  1. Different API key used for generation vs. split — Files are scoped to a company. Use the same API key (same company) for both steps.
  2. File expired before splitting — If expiration_hours passed since generation, the file is no longer available. Generate with a longer expiry, or split immediately after generation.
  3. Typo in filename — Use the exact data.filename value returned by the generate endpoint.

Invalid page range (422)

{
  "success": false,
  "message": "Invalid page range. The PDF has 10 pages."
}

Page numbers must be between 1 and the total page count. Page numbers in a range must be in ascending order (1-5 is valid; 5-1 is not). Check the source PDF page count before calling the split endpoint if page numbers are dynamic.

File expired as an intermediate step

When chaining split with compress or merge, intermediate files can expire if pipeline steps are slow. Use a short but adequate expiration_hours on intermediate results:

# Intermediate split file: 1 hour is enough for a compress step that runs immediately
split_result = split_pdf(filename, pages="1-5", expiration_hours=1)
# Proceed immediately to the next step
compress_result = compress_pdf(split_result["data"]["filename"])

Related APIs and Guides

Operation Endpoint Guide
Generate PDF POST /api/pdf/generate PDF API Quickstart
Merge PDFs POST /api/pdf/merge PDF Merge API Guide
Compress PDF POST /api/pdf/compress PDF Compress API Guide
Download file GET /api/pdf/download/{filename} API Documentation
PDF manipulation overview PDF Manipulation Guide

For the full API reference and authentication details, see the API Documentation. To test split interactively before writing code, use the FUNBREW PDF Playground. For real-world PDF automation patterns including bulk certificate extraction, see the Use Cases section.


Summary

Key points for the FUNBREW PDF split API:

  1. Two-step workflow: Generate a PDF with POST /api/pdf/generate, then split it with POST /api/pdf/split using the returned filename.
  2. Flexible page spec: Combine ranges (1-5) and individual pages (7,9) in any order in the pages string.
  3. Pages are deduplicated: If you list the same page twice, it appears only once in the output.
  4. Invalid range = 422: Any page number outside the PDF's actual page count causes the entire request to fail — validate page counts when using dynamic inputs.
  5. Chain with merge and compress: Split, then merge selected pages, or compress the extracted result for email delivery.
  6. SaaS only: The pdf.feature:split gate must be enabled on your plan.
Powered by FUNBREW PDF