2026/05/15

PDF結合API完全ガイド:複数PDFを一本化するコード例付き

PDF結合PDF操作PDF APINode.jsPython

請求書を月次レポートにまとめたい、複数の契約書を1つのドキュメントセットにしたい、イベントの参加証をまとめて一括配布したい――こうしたPDF結合のニーズは、Webアプリ・バックオフィスツール・SaaSプラットフォームを問わず頻繁に発生します。

FUNBREW PDF の PDF結合API(POST /api/pdf/merge)を使えば、2〜50のPDFファイルをAPIコール1本でマージし、ダウンロード用URLとして受け取ることができます。ローカルSDK(PyPDF2・iTextなど)をサーバーにインストールする必要はなく、REST APIへのHTTPリクエストだけで完結します。

このガイドでは、APIの仕様から実際のコード例(cURL・Node.js・Python・PHP)、大量PDF結合のベストプラクティス、よくある失敗と対処法までを解説します。

PDF生成からS3保存まで含むフルパイプラインはPDF一括生成ガイドを参照してください。証明書PDFの自動化は証明書PDF自動化ガイドで詳しく解説しています。

PDF結合APIの仕様

FUNBREW PDF はPDF結合用に2つのエンドポイントを提供します。

エンドポイント1: サーバー上のファイルを結合する

POST /api/pdf/merge
Content-Type: application/json
Authorization: Bearer {APIキー}

リクエストボディ

パラメータ 必須 説明
filenames string[] 必須 結合するファイル名のリスト(2〜50個)。事前に /api/pdf/generate などで生成・保存済みのファイル名を指定する
expiration_hours integer 任意 結合済みファイルの有効期限(0〜168時間、デフォルト24時間)
max_downloads integer 任意 最大ダウンロード回数(0〜100、デフォルト10)
watermark string 任意 ウォーターマークテキスト(最大255文字)

レスポンス(成功時)

{
  "success": true,
  "data": {
    "filename": "merged-abc123.pdf",
    "download_url": "https://pdf.funbrew.cloud/api/pdf/download/merged-abc123.pdf",
    "file_size": 204800,
    "expires_at": "2026-05-16T10:00:00Z"
  }
}

エンドポイント2: ファイルをアップロードして結合する

POST /api/pdf/merge-upload
Content-Type: multipart/form-data
Authorization: Bearer {APIキー}

リクエストパラメータ

パラメータ 必須 説明
files[] file 任意 アップロードするPDFファイル(各50MB以下、最大50ファイル)
filenames[] string[] 任意 サーバー上の既存ファイル名(files[] と組み合わせ可能)
expiration_hours integer 任意 有効期限(0〜168時間、デフォルト24時間)
max_downloads integer 任意 最大ダウンロード回数(0〜100、デフォルト10)
watermark string 任意 ウォーターマークテキスト

files[]filenames[] の合計が2ファイル以上である必要があります。

エラーレスポンス

HTTPステータス 原因
401 Unauthorized APIキーが無効
403 Forbidden pdf.feature:merge が無効なプラン
422 Unprocessable Entity ファイルが見つからない・期限切れ・暗号化されている

コード例

cURL(サーバーファイルを結合)

まずPDFを生成してサーバーに保存し、そのファイル名を使って結合します。

# 手順1: 1つ目のPDFを生成(ファイル名を記録する)
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>請求書 #001</h1><p>2026年5月分</p>",
    "options": { "format": "A4", "responseFormat": "url" }
  }')
FILE1=$(echo $RESPONSE | jq -r '.data.filename')

# 手順2: 2つ目のPDFを生成
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>請求書 #002</h1><p>2026年5月分</p>",
    "options": { "format": "A4", "responseFormat": "url" }
  }')
FILE2=$(echo $RESPONSE | jq -r '.data.filename')

# 手順3: 2つのPDFを結合する
curl -s -X POST https://pdf.funbrew.cloud/api/pdf/merge \
  -H "Authorization: Bearer $FUNBREW_PDF_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"filenames\": [\"$FILE1\", \"$FILE2\"],
    \"expiration_hours\": 48,
    \"max_downloads\": 5
  }" | jq .

cURL(ファイルをアップロードして結合)

ローカルにPDFファイルがある場合は merge-upload エンドポイントを使います。

curl -s -X POST https://pdf.funbrew.cloud/api/pdf/merge-upload \
  -H "Authorization: Bearer $FUNBREW_PDF_API_KEY" \
  -F "files[]=@invoice-001.pdf" \
  -F "files[]=@invoice-002.pdf" \
  -F "files[]=@invoice-003.pdf" \
  -F "expiration_hours=72" \
  -F "max_downloads=10" | jq .

Node.js(サーバーファイルを結合)

const fs = require("fs");
const path = require("path");

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

/**
 * FUNBREW PDFでPDFを生成し、サーバー上のファイル名を返す
 */
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; // サーバー上のファイル名
}

/**
 * サーバー上のPDFファイル群を結合する
 */
async function mergePdfs(filenames, options = {}) {
  const response = await fetch(`${BASE_URL}/api/pdf/merge`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      filenames,
      expiration_hours: options.expirationHours ?? 24,
      max_downloads: options.maxDownloads ?? 10,
      ...(options.watermark ? { watermark: options.watermark } : {}),
    }),
  });

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

  return response.json(); // { success, data: { filename, download_url, file_size, expires_at } }
}

// 使用例: 請求書3件を生成して結合する
async function generateAndMergeInvoices(invoices) {
  console.log(`${invoices.length}件の請求書を生成中...`);

  // 並列生成(セマフォなしで最大10件程度まで)
  const filenames = await Promise.all(
    invoices.map((inv) =>
      generatePdf(`
        <h1 style="font-family: sans-serif;">請求書 ${inv.number}</h1>
        <p>得意先: ${inv.client}</p>
        <p>金額: ¥${inv.amount.toLocaleString()}</p>
        <p>発行日: ${inv.date}</p>
      `)
    )
  );

  console.log(`${filenames.length}件を生成しました。結合中...`);

  const result = await mergePdfs(filenames, {
    expirationHours: 48,
    maxDownloads: 5,
  });

  console.log("結合完了:", result.data.download_url);
  return result.data;
}

// 実行
generateAndMergeInvoices([
  { number: "#001", client: "株式会社A", amount: 150000, date: "2026-05-15" },
  { number: "#002", client: "株式会社B", amount: 280000, date: "2026-05-15" },
  { number: "#003", client: "株式会社C", amount: 95000, date: "2026-05-15" },
]).then(console.log).catch(console.error);

Node.js(ローカルファイルをアップロードして結合)

const FormData = require("form-data");
const fs = require("fs");
const fetch = require("node-fetch"); // node-fetch v2系の場合

async function mergeLocalPdfs(filePaths, options = {}) {
  const form = new FormData();

  for (const filePath of filePaths) {
    form.append("files[]", fs.createReadStream(filePath), {
      filename: require("path").basename(filePath),
      contentType: "application/pdf",
    });
  }

  if (options.expirationHours !== undefined) {
    form.append("expiration_hours", String(options.expirationHours));
  }
  if (options.maxDownloads !== undefined) {
    form.append("max_downloads", String(options.maxDownloads));
  }
  if (options.watermark) {
    form.append("watermark", options.watermark);
  }

  const response = await fetch("https://pdf.funbrew.cloud/api/pdf/merge-upload", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
      ...form.getHeaders(),
    },
    body: form,
  });

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

  return response.json();
}

// 使用例
mergeLocalPdfs(
  ["reports/january.pdf", "reports/february.pdf", "reports/march.pdf"],
  { expirationHours: 72, maxDownloads: 3, watermark: "CONFIDENTIAL" }
).then((result) => {
  console.log("Download URL:", result.data.download_url);
  console.log("File size:", result.data.file_size, "bytes");
});

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:
    """PDFを生成してサーバー上のファイル名を返す"""
    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 merge_pdfs(
    filenames: list[str],
    expiration_hours: int = 24,
    max_downloads: int = 10,
    watermark: Optional[str] = None,
) -> dict:
    """サーバー上のPDFファイル群を結合する"""
    payload: dict = {
        "filenames": filenames,
        "expiration_hours": expiration_hours,
        "max_downloads": max_downloads,
    }
    if watermark:
        payload["watermark"] = watermark

    response = httpx.post(
        f"{BASE_URL}/api/pdf/merge",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json=payload,
        timeout=300,  # 大量ファイルの結合は時間がかかる場合がある
    )
    response.raise_for_status()
    return response.json()


# 使用例: 月次レポート3部を生成して結合する
def bundle_monthly_reports(month: str, departments: list[dict]) -> dict:
    """部門別レポートを生成して1つのPDFにバンドルする"""
    filenames = []

    for dept in departments:
        html = f"""
        <html>
        <body style="font-family: sans-serif; padding: 40px;">
          <h1>{dept['name']} 月次レポート</h1>
          <p>対象期間: {month}</p>
          <p>売上: ¥{dept['revenue']:,}</p>
          <p>目標達成率: {dept['achievement_rate']:.1%}</p>
        </body>
        </html>
        """
        filename = generate_pdf(html)
        filenames.append(filename)
        print(f"生成: {dept['name']} → {filename}")

    print(f"{len(filenames)}件を結合中...")
    result = merge_pdfs(filenames, expiration_hours=72, max_downloads=5)

    print(f"結合完了: {result['data']['download_url']}")
    return result["data"]


# 実行例
departments = [
    {"name": "営業部", "revenue": 12_500_000, "achievement_rate": 1.08},
    {"name": "マーケティング部", "revenue": 5_200_000, "achievement_rate": 0.95},
    {"name": "開発部", "revenue": 8_000_000, "achievement_rate": 1.15},
]
result = bundle_monthly_reports("2026年4月", departments)

Python(ローカルファイルをアップロードして結合)

import os
import httpx


def merge_local_pdfs(
    file_paths: list[str],
    expiration_hours: int = 24,
    max_downloads: int = 10,
    watermark: str | None = None,
) -> dict:
    """ローカルのPDFファイル群をアップロードして結合する"""
    files = []
    for path in file_paths:
        files.append(
            ("files[]", (os.path.basename(path), open(path, "rb"), "application/pdf"))
        )

    data: dict[str, str] = {
        "expiration_hours": str(expiration_hours),
        "max_downloads": str(max_downloads),
    }
    if watermark:
        data["watermark"] = watermark

    response = httpx.post(
        "https://pdf.funbrew.cloud/api/pdf/merge-upload",
        headers={"Authorization": f"Bearer {os.environ['FUNBREW_PDF_API_KEY']}"},
        files=files,
        data=data,
        timeout=300,
    )
    response.raise_for_status()
    return response.json()


# 使用例
result = merge_local_pdfs(
    ["reports/q1.pdf", "reports/q2.pdf", "reports/q3.pdf"],
    expiration_hours=168,  # 7日間有効
    max_downloads=3,
    watermark="DRAFT",
)
print("ダウンロードURL:", result["data"]["download_url"])
print("ファイルサイズ:", result["data"]["file_size"], "bytes")

PHP

<?php

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

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

    /**
     * サーバー上のPDFを結合する
     *
     * @param string[] $filenames  結合するファイル名のリスト
     * @param int      $expirationHours  有効期限(時間)
     * @param int      $maxDownloads     最大ダウンロード回数
     * @param string|null $watermark    ウォーターマークテキスト
     */
    public function merge(
        array $filenames,
        int $expirationHours = 24,
        int $maxDownloads = 10,
        ?string $watermark = null
    ): array {
        $payload = [
            'filenames'        => $filenames,
            'expiration_hours' => $expirationHours,
            'max_downloads'    => $maxDownloads,
        ];
        if ($watermark !== null) {
            $payload['watermark'] = $watermark;
        }

        $ch = curl_init("{$this->baseUrl}/api/pdf/merge");
        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        => 300,
        ]);

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

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

        return json_decode($body, true);
    }

    /**
     * ローカルのPDFファイルをアップロードして結合する
     *
     * @param string[] $filePaths  ローカルファイルパスのリスト
     */
    public function mergeUpload(
        array $filePaths,
        int $expirationHours = 24,
        int $maxDownloads = 10,
        ?string $watermark = null
    ): array {
        $postFields = [
            'expiration_hours' => $expirationHours,
            'max_downloads'    => $maxDownloads,
        ];
        if ($watermark !== null) {
            $postFields['watermark'] = $watermark;
        }

        foreach ($filePaths as $i => $path) {
            $postFields["files[{$i}]"] = new \CURLFile($path, 'application/pdf', basename($path));
        }

        $ch = curl_init("{$this->baseUrl}/api/pdf/merge-upload");
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $postFields,
            CURLOPT_HTTPHEADER     => [
                "Authorization: Bearer {$this->apiKey}",
            ],
            CURLOPT_TIMEOUT        => 300,
        ]);

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

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

        return json_decode($body, true);
    }
}

// 使用例(Laravel)
$merger = new FunbrewPdfMerger(env('FUNBREW_PDF_API_KEY'));

// サーバーファイルを結合
$result = $merger->merge(
    filenames:      ['invoice-001.pdf', 'invoice-002.pdf', 'invoice-003.pdf'],
    expirationHours: 48,
    maxDownloads:   5
);

echo "ダウンロードURL: " . $result['data']['download_url'] . "\n";
echo "ファイルサイズ: " . $result['data']['file_size'] . " bytes\n";

ユースケース別の実装パターン

請求書バンドル(月末一括送付)

月末に全顧客への請求書を生成して1件ずつ個別に送るのではなく、部署ごとや顧客ごとにバンドルして送付するパターンです。

import asyncio
import aiohttp

async def generate_invoice_async(session: aiohttp.ClientSession, invoice_data: dict, api_key: str) -> str:
    """請求書PDFを非同期で生成してファイル名を返す"""
    html = f"""
    <html>
    <body style="font-family: sans-serif; padding: 40px;">
      <h1>請求書 #{invoice_data['number']}</h1>
      <table>
        <tr><th>品目</th><th>数量</th><th>金額</th></tr>
        {''.join(f"<tr><td>{item['name']}</td><td>{item['qty']}</td><td>¥{item['price']:,}</td></tr>" for item in invoice_data['items'])}
      </table>
      <p><strong>合計: ¥{invoice_data['total']:,}</strong></p>
    </body>
    </html>
    """

    async with session.post(
        "https://pdf.funbrew.cloud/api/pdf/generate",
        headers={"Authorization": f"Bearer {api_key}"},
        json={"html": html, "options": {"format": "A4", "responseFormat": "url"}},
        timeout=aiohttp.ClientTimeout(total=120),
    ) as resp:
        resp.raise_for_status()
        data = await resp.json()
        return data["data"]["filename"]


async def bundle_invoices_for_client(client_id: str, invoices: list[dict], api_key: str) -> str:
    """クライアントの全請求書を生成して1つにバンドルする"""
    async with aiohttp.ClientSession() as session:
        sem = asyncio.Semaphore(5)

        async def limited_generate(inv):
            async with sem:
                return await generate_invoice_async(session, inv, api_key)

        filenames = await asyncio.gather(*[limited_generate(inv) for inv in invoices])

    # 結合
    response = httpx.post(
        "https://pdf.funbrew.cloud/api/pdf/merge",
        headers={"Authorization": f"Bearer {api_key}"},
        json={"filenames": list(filenames), "expiration_hours": 72},
        timeout=300,
    )
    response.raise_for_status()
    return response.json()["data"]["download_url"]

契約書セットの作成

複数の関連ドキュメント(本契約・覚書・別紙・印鑑証明など)を1つのPDFにまとめる場合です。

async function createContractPackage(contractData) {
  const API_KEY = process.env.FUNBREW_PDF_API_KEY;

  // 各ドキュメントを生成
  const documents = [
    { name: "main-contract", html: buildMainContractHtml(contractData) },
    { name: "memo", html: buildMemorandumHtml(contractData) },
    { name: "appendix", html: buildAppendixHtml(contractData) },
  ];

  const filenames = await Promise.all(
    documents.map(async (doc) => {
      const res = await fetch("https://pdf.funbrew.cloud/api/pdf/generate", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          html: doc.html,
          options: { format: "A4", responseFormat: "url" },
        }),
      });
      const { data } = await res.json();
      return data.filename;
    })
  );

  // 1つのPDFに結合(7日間有効・ダウンロード2回まで)
  const mergeRes = await fetch("https://pdf.funbrew.cloud/api/pdf/merge", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      filenames,
      expiration_hours: 168, // 7日間
      max_downloads: 2,      // 両者それぞれ1回ずつ
      watermark: "DRAFT",    // 本署名前のウォーターマーク
    }),
  });

  const { data } = await mergeRes.json();
  return data.download_url;
}

レポート結合(四半期・年次まとめ)

月次レポートを四半期レポートにまとめる定期バッチ処理パターンです。

from datetime import date

def merge_quarterly_reports(year: int, quarter: int, api_key: str) -> str:
    """指定四半期の月次レポートPDFを取得して結合する"""
    # 四半期の月を計算
    start_month = (quarter - 1) * 3 + 1
    months = [start_month, start_month + 1, start_month + 2]

    # 各月のレポートのファイル名をDBから取得(実装は環境依存)
    filenames = []
    for month in months:
        report_date = date(year, month, 1)
        filename = get_monthly_report_filename(report_date)  # DBまたはS3から取得
        if filename:
            filenames.append(filename)

    if len(filenames) < 2:
        raise ValueError(f"結合に必要なレポートが不足しています: {len(filenames)}件")

    import httpx
    response = httpx.post(
        "https://pdf.funbrew.cloud/api/pdf/merge",
        headers={"Authorization": f"Bearer {api_key}"},
        json={
            "filenames": filenames,
            "expiration_hours": 168,  # 7日間保持
            "max_downloads": 50,      # 全社員がダウンロード可能
        },
        timeout=300,
    )
    response.raise_for_status()
    return response.json()["data"]["download_url"]

大量PDF結合のベストプラクティス

1. 50ファイルを超える場合は2段階結合を使う

APIの上限は1リクエストあたり50ファイルです。それを超える場合はバッチに分割して結合し、結果をさらに結合する2段階アプローチを使います。

import math

def merge_large_batch(filenames: list[str], api_key: str, batch_size: int = 40) -> str:
    """50ファイルを超える場合でも2段階結合で処理する"""
    if len(filenames) <= batch_size:
        result = merge_pdfs(filenames, api_key)
        return result["data"]["filename"]

    # 第1段階: バッチに分割して結合
    batches = [filenames[i:i+batch_size] for i in range(0, len(filenames), batch_size)]
    intermediate_filenames = []

    for i, batch in enumerate(batches):
        print(f"バッチ {i+1}/{len(batches)} を結合中({len(batch)}件)...")
        result = merge_pdfs(batch, api_key, expiration_hours=2)  # 一時ファイルは2時間保持
        intermediate_filenames.append(result["data"]["filename"])

    # 第2段階: 中間ファイルを最終結合
    print(f"最終結合: {len(intermediate_filenames)}件の中間ファイルを統合...")
    final_result = merge_pdfs(intermediate_filenames, api_key)
    return final_result["data"]["download_url"]

2. 生成と結合のパイプラインを最適化する

PDFの生成フェーズと結合フェーズを分離して、生成は並列・結合は逐次で処理するとスループットが上がります。

import asyncio
import aiohttp

async def generate_all_async(html_list: list[str], api_key: str, concurrency: int = 5) -> list[str]:
    """複数のHTMLを並列でPDF化する"""
    sem = asyncio.Semaphore(concurrency)

    async def generate_one(session, html):
        async with sem:
            async with session.post(
                "https://pdf.funbrew.cloud/api/pdf/generate",
                headers={"Authorization": f"Bearer {api_key}"},
                json={"html": html, "options": {"format": "A4", "responseFormat": "url"}},
                timeout=aiohttp.ClientTimeout(total=120),
            ) as resp:
                resp.raise_for_status()
                data = await resp.json()
                return data["data"]["filename"]

    async with aiohttp.ClientSession() as session:
        filenames = await asyncio.gather(*[generate_one(session, h) for h in html_list])

    return list(filenames)


def generate_and_merge_pipeline(html_list: list[str], api_key: str) -> str:
    """生成→結合パイプライン"""
    # 並列生成
    filenames = asyncio.run(generate_all_async(html_list, api_key))
    print(f"{len(filenames)}件のPDFを生成完了")

    # 結合(50件ずつバッチ処理)
    return merge_large_batch(filenames, api_key)

3. ページ順序の管理

filenames または files[] の配列の順序がそのままPDFのページ順序になります。事前にリストをソートして意図した順序を確保してください。

# 請求書番号でソートして結合する
filenames_with_meta = [
    {"filename": "invoice-003.pdf", "invoice_no": 3},
    {"filename": "invoice-001.pdf", "invoice_no": 1},
    {"filename": "invoice-002.pdf", "invoice_no": 2},
]
sorted_filenames = [
    f["filename"]
    for f in sorted(filenames_with_meta, key=lambda x: x["invoice_no"])
]
# → ["invoice-001.pdf", "invoice-002.pdf", "invoice-003.pdf"]

4. 有効期限とダウンロード制限の設計

ユースケース expiration_hours max_downloads 理由
契約書セット(相互署名前) 168(7日) 2 両者が1回ずつ
社内配布レポート 720(30日) 100 全社員がアクセス可能
一時的な確認用 4 3 確認後は不要
アーカイブ保存 0(永久) 0(無制限) 長期保存

よくある失敗と対処法

ファイルが見つからない(422エラー)

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

原因と対処:

  1. ファイルが別のAPIキー(会社)で生成されている: ファイルは会社単位で管理されます。生成に使ったAPIキーと結合に使うAPIキーが同じ会社のものであることを確認してください。
  2. ファイルが期限切れ: 生成時の expiration_hours を過ぎると結合できません。生成直後に結合する、または十分に長い有効期限を設定してください。
  3. ファイル名が間違っている: generate APIのレスポンス data.filename の値をそのまま使ってください(拡張子含む)。

暗号化PDFのエラー

暗号化されたPDFはFPDI(使用しているPDFライブラリ)が解析できないため結合できません。

Error: This PDF document probably uses a compression technique which is not supported by the free parser shipped with FPDI.

対処: 結合前にPDFパスワードを解除してください。ローカルであれば qpdf コマンドが利用できます。

# qpdf でパスワードを解除する
qpdf --password="your-password" --decrypt encrypted.pdf decrypted.pdf

PDFバージョン非互換エラー

PDF 2.0形式のファイルは古いパーサーで処理できない場合があります。Chromiumで生成したPDFは通常PDF 1.7以下ですが、外部ツールで作成したPDFが問題になることがあります。

# PDFバージョンを確認する
with open("document.pdf", "rb") as f:
    header = f.read(8).decode("ascii", errors="ignore")
    print(f"PDF version: {header}")  # %PDF-1.7 のように表示される

PDF 2.0ファイルを含む場合は、まず変換ツール(Ghostscript等)でPDF 1.7互換に変換してからアップロードしてください。

# Ghostscriptでバージョン変換
gs -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -dCompatibilityLevel=1.7 \
   -sOutputFile=converted.pdf input.pdf

サイズ制限超過

merge-upload エンドポイントでは各ファイルが50MB以下である必要があります。大きなPDFは事前に圧縮してください。

# Ghostscriptで圧縮する
gs -dBATCH -dNOPAUSE -sDEVICE=pdfwrite \
   -dCompatibilityLevel=1.7 \
   -dPDFSETTINGS=/ebook \  # /screen(小) /ebook(中) /printer(大) /prepress(最高品質)
   -sOutputFile=compressed.pdf input.pdf

タイムアウト

多数のファイルや大容量ファイルを結合する場合は処理時間が長くなります。HTTPクライアントのタイムアウトを300秒以上に設定してください。

# httpxの場合
response = httpx.post(url, ..., timeout=300)

# requestsの場合
response = requests.post(url, ..., timeout=300)

関連API

FUNBREW PDFはPDF結合以外にも以下の操作をAPIで提供しています。

API エンドポイント 用途
PDF生成 POST /api/pdf/generate HTMLからPDFを生成
PDF結合 POST /api/pdf/merge 複数PDFを1ファイルに結合
ダウンロード GET /api/pdf/download/{filename} 生成・結合済みファイルのダウンロード

PDF生成の詳細はAPIドキュメントを参照してください。複数のPDFを個別にファイルとしてダウンロードするパターンはPDF一括生成ガイドで解説しています。証明書PDFの一括発行パイプラインは証明書PDF自動化ガイドを参照してください。


まとめ

FUNBREW PDF の PDF結合APIのポイントを整理します。

  1. 2つのエンドポイント: POST /api/pdf/merge(サーバーファイル)と POST /api/pdf/merge-upload(ローカルアップロード)を使い分ける
  2. 最大50ファイル: 超える場合は2段階結合(バッチ分割→最終結合)を使う
  3. 有効期限とダウンロード制限: ユースケースに応じて expiration_hoursmax_downloads を適切に設定する
  4. ページ順序: 配列の順序がそのままPDFのページ順になるため、事前にソートする
  5. よくある失敗: 暗号化PDF・バージョン非互換・サイズ超過・ファイル期限切れに注意する
  6. 並列生成 + 結合: 生成フェーズを非同期並列化し、結合フェーズをシリアル処理することでスループットを最大化する

FUNBREW PDF PlaygroundでPDF生成を試してから結合APIを組み合わせることをおすすめします。APIの完全な仕様はAPIドキュメントを参照してください。イベント向けの証明書一括発行についてはイベント証明書一括発行ガイドも参考にしてください。

Powered by FUNBREW PDF