証明書PDF SDK クイックスタート|最初のPDF証明書を5分で生成する
「証明書PDFを自動生成したいがどこから始めればいいかわからない」――このガイドはそういった方向けの出発点です。
FUNBREW PDF API を使って最初の証明書PDFを生成するまでの最短ルートを、Node.js・Python・cURLのコード例付きで解説します。まず動くものを作り、そのあとで本格的なSDKパターンへ発展させる流れです。
証明書クラスターの全体像を先に把握したい場合は証明書PDF自動化ガイドを参照してください。SDKの設計(リトライ・HMAC改ざん防止ID・複数テンプレート管理)に進む準備ができたら証明書PDF生成SDKガイドを参照してください。
最初の証明書PDFを生成する
APIキーはFUNBREW PDFのダッシュボードから発行できます。
cURL(動作確認用)
curl -s -X POST https://pdf.funbrew.cloud/api/pdf/generate \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{
"html": "<div style=\"font-family: sans-serif; text-align: center; padding: 80px;\"><h1>修了証</h1><p>山田 太郎</p><p>Webアプリケーション開発コース</p><p>2026年5月10日</p></div>",
"options": { "format": "A4", "landscape": true }
}' \
--output certificate.pdf
数秒でローカルに certificate.pdf が生成されます。
JavaScript (Node.js)
const fs = require("fs");
async function generateCertificate({ name, course, date }) {
const html = `
<div style="font-family: 'Noto Sans JP', sans-serif; text-align: center; padding: 80px 60px; border: 8px double #b8860b;">
<p style="font-size: 14px; color: #6b7280; letter-spacing: 0.2em;">CERTIFICATE OF COMPLETION</p>
<h1 style="font-size: 28px; margin: 24px 0 8px;">修了証</h1>
<p style="font-size: 20px; font-weight: 700; margin: 32px 0;">${name}</p>
<p style="font-size: 14px; color: #4b5563;">上記の者は以下のコースを修了したことを証明します。</p>
<p style="font-size: 18px; font-weight: 600; margin: 24px 0;">${course}</p>
<p style="font-size: 13px; color: #9ca3af; margin-top: 48px;">${date}</p>
</div>
`;
const response = await fetch("https://pdf.funbrew.cloud/api/pdf/generate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: { format: "A4", landscape: true, printBackground: true },
}),
});
if (!response.ok) {
throw new Error(`PDF generation failed: ${response.status}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
fs.writeFileSync("certificate.pdf", buffer);
console.log("certificate.pdf を生成しました");
}
// 実行
generateCertificate({
name: "山田 太郎",
course: "Webアプリケーション開発コース",
date: "2026年5月10日",
});
Python
import os
import httpx
def generate_certificate(name: str, course: str, date: str) -> bytes:
html = f"""
<div style="font-family: 'Noto Sans JP', sans-serif; text-align: center; padding: 80px 60px; border: 8px double #b8860b;">
<p style="font-size: 14px; color: #6b7280; letter-spacing: 0.2em;">CERTIFICATE OF COMPLETION</p>
<h1 style="font-size: 28px; margin: 24px 0 8px;">修了証</h1>
<p style="font-size: 20px; font-weight: 700; margin: 32px 0;">{name}</p>
<p style="font-size: 14px; color: #4b5563;">上記の者は以下のコースを修了したことを証明します。</p>
<p style="font-size: 18px; font-weight: 600; margin: 24px 0;">{course}</p>
<p style="font-size: 13px; color: #9ca3af; margin-top: 48px;">{date}</p>
</div>
"""
response = httpx.post(
"https://pdf.funbrew.cloud/api/pdf/generate",
headers={"Authorization": f"Bearer {os.environ['FUNBREW_PDF_API_KEY']}"},
json={
"html": html,
"options": {"format": "A4", "landscape": True, "printBackground": True},
},
)
response.raise_for_status()
return response.content
pdf_bytes = generate_certificate("山田 太郎", "Webアプリケーション開発コース", "2026年5月10日")
with open("certificate.pdf", "wb") as f:
f.write(pdf_bytes)
print("certificate.pdf を生成しました")
テンプレートを使った証明書生成
インラインHTMLは手軽ですが、受講者ごとに文字列連結するのはスケールしません。次のステップとして、HTMLテンプレートとデータ注入パターンを採用します。
テンプレートファイル(templates/certificate.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@400;700&display=swap" rel="stylesheet">
<style>
body { margin: 0; background: #fffdf5; }
.cert {
width: 277mm; height: 190mm;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
border: 10px double #b8860b;
padding: 40px;
box-sizing: border-box;
font-family: 'Noto Serif JP', serif;
}
.label { font-size: 11px; letter-spacing: 0.3em; color: #9ca3af; }
.title { font-size: 32px; margin: 16px 0; }
.name { font-size: 26px; font-weight: 700; margin: 24px 0 8px; }
.course { font-size: 18px; color: #374151; margin: 16px 0 32px; }
.date { font-size: 12px; color: #9ca3af; }
</style>
</head>
<body>
<div class="cert">
<p class="label">CERTIFICATE OF COMPLETION</p>
<h1 class="title">修了証</h1>
<p class="name">{{name}}</p>
<p class="label">上記の者は以下のコースを修了したことを証明します。</p>
<p class="course">{{course}}</p>
<p class="date">{{date}} 発行</p>
</div>
</body>
</html>
Node.js でテンプレートに差し込む
const fs = require("fs");
async function renderCertificate(templatePath, data) {
let html = fs.readFileSync(templatePath, "utf-8");
for (const [key, value] of Object.entries(data)) {
html = html.replaceAll(`{{${key}}}`, value);
}
const response = await fetch("https://pdf.funbrew.cloud/api/pdf/generate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: { format: "A4", landscape: true, printBackground: true },
}),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return Buffer.from(await response.arrayBuffer());
}
// 使用例
const pdf = await renderCertificate("templates/certificate.html", {
name: "山田 太郎",
course: "Webアプリケーション開発コース",
date: "2026年5月10日",
});
fs.writeFileSync("certificate.pdf", pdf);
Python で Jinja2 を使う
import os
import httpx
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("templates"))
def render_certificate(name: str, course: str, date: str) -> bytes:
template = env.get_template("certificate.html")
html = template.render(name=name, course=course, date=date)
response = httpx.post(
"https://pdf.funbrew.cloud/api/pdf/generate",
headers={"Authorization": f"Bearer {os.environ['FUNBREW_PDF_API_KEY']}"},
json={
"html": html,
"options": {"format": "A4", "landscape": True, "printBackground": True},
},
)
response.raise_for_status()
return response.content
API レスポンスのパターン
FUNBREW PDF API は2つのモードで動作します。
バイナリレスポンス(即時ダウンロード)
// options.responseFormat を指定しない場合はバイナリ(application/pdf)が返る
const buffer = Buffer.from(await response.arrayBuffer());
fs.writeFileSync("certificate.pdf", buffer);
JSON レスポンス(URL取得)
// options.responseFormat: "url" を指定するとダウンロードURLが返る
const body = JSON.stringify({
html,
options: { format: "A4", landscape: true, responseFormat: "url" },
});
const { data } = await response.json();
console.log("ダウンロードURL:", data.download_url);
// → https://pdf.funbrew.cloud/dl/abc123...(有効期限付き)
次のステップ
複数の受講者に一括発行したい
CSVから100〜10,000枚を並列生成するパターンは証明書一括発行ガイドで解説しています。
リトライ・改ざん防止IDを実装したい
本番品質のSDKラッパー設計(指数バックオフ・HMAC証明書ID・複数テンプレート管理)は証明書PDF生成SDKガイドを参照してください。
システムに組み込みたい(LMS・HRシステム)
LMSのWebhookと連携したパイプライン設計は証明書API連携ガイドを参照してください。
テンプレートをもっと凝ったデザインにしたい
コピペ用のHTMLテンプレート5種はHTML証明書テンプレート集にあります。
証明書PDF生成を今すぐ試すにはFUNBREW PDF Playgroundが最も手軽です。APIを使い始めるにはAPIドキュメントを参照してください。
PHP での証明書生成
PHPスタック(Laravel・Symfony・WordPressなど)で証明書を生成する場合も同じパターンで実装できます。
PHP(単一証明書)
<?php
function generateCertificate(string $name, string $course, string $date): string
{
$html = <<<HTML
<div style="
font-family: 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', sans-serif;
text-align: center;
padding: 80px 60px;
border: 8px double #b8860b;
">
<p style="font-size: 11px; letter-spacing: 0.3em; color: #9ca3af;">CERTIFICATE OF COMPLETION</p>
<h1 style="font-size: 30px; margin: 20px 0;">修了証</h1>
<p style="font-size: 22px; font-weight: 700; margin: 28px 0;">{$name}</p>
<p style="font-size: 14px; color: #6b7280;">上記の者は以下のコースを修了したことを証明します。</p>
<p style="font-size: 18px; font-weight: 600; margin: 20px 0 40px;">{$course}</p>
<p style="font-size: 12px; color: #9ca3af;">{$date} 発行</p>
</div>
HTML;
$response = \Illuminate\Support\Facades\Http::withToken(env('FUNBREW_PDF_API_KEY'))
->post('https://pdf.funbrew.cloud/api/pdf/generate', [
'html' => $html,
'options' => [
'format' => 'A4',
'landscape' => true,
'printBackground' => true,
],
]);
if ($response->failed()) {
throw new \RuntimeException("PDF generation failed: HTTP {$response->status()}");
}
return $response->body(); // PDF バイト列
}
// 使用例
$pdfBytes = generateCertificate('山田 太郎', 'Webアプリケーション開発コース', '2026年5月15日');
file_put_contents('certificate.pdf', $pdfBytes);
echo "certificate.pdf を生成しました\n";
PHP(Laravel Artisan コマンドでバッチ発行)
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
class GenerateCertificates extends Command
{
protected $signature = 'certificates:generate {--csv= : CSVファイルパス}';
protected $description = '受講者CSVから証明書PDFを一括生成する';
public function handle(): int
{
$csvPath = $this->option('csv') ?? storage_path('app/recipients.csv');
$rows = array_map('str_getcsv', file($csvPath));
$headers = array_shift($rows); // 1行目はヘッダー
$bar = $this->output->createProgressBar(count($rows));
$bar->start();
foreach ($rows as $row) {
$data = array_combine($headers, $row);
$this->generateAndSave(
name: $data['name'],
course: $data['course'],
date: $data['date'],
certId: $data['cert_id'],
);
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info(count($rows) . " 件の証明書を生成しました。");
return Command::SUCCESS;
}
private function generateAndSave(string $name, string $course, string $date, string $certId): void
{
$html = view('certificates.template', compact('name', 'course', 'date', 'certId'))->render();
$response = Http::withToken(config('services.funbrew_pdf.api_key'))
->timeout(120)
->post('https://pdf.funbrew.cloud/api/pdf/generate', [
'html' => $html,
'options' => ['format' => 'A4', 'landscape' => true, 'printBackground' => true],
]);
$response->throw(); // 失敗時は例外を投げる
$path = storage_path("app/certificates/cert-{$certId}.pdf");
file_put_contents($path, $response->body());
}
}
エラーハンドリングとリトライ
本番環境では、ネットワーク障害・タイムアウト・レート制限など一時的なエラーが発生します。適切なリトライ戦略で証明書の取りこぼしを防ぎます。
Node.js(指数バックオフ付きリトライ)
const fs = require("fs");
/**
* 指数バックオフ付きリトライ関数
* @param {Function} fn - 実行する非同期関数
* @param {number} retries - 最大リトライ回数(デフォルト3)
* @param {number} delay - 初回待機ミリ秒(デフォルト1000ms)
*/
async function withRetry(fn, retries = 3, delay = 1000) {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await fn();
} catch (err) {
const isLast = attempt === retries;
const isRetryable = err.message.includes("429") || err.message.includes("503") || err.message.includes("timeout");
if (isLast || !isRetryable) throw err;
const wait = delay * Math.pow(2, attempt) + Math.random() * 500; // ジッター付き
console.warn(`[リトライ ${attempt + 1}/${retries}] ${wait.toFixed(0)}ms 後に再試行...`);
await new Promise((r) => setTimeout(r, wait));
}
}
}
async function generateCertificateWithRetry({ name, course, date, certId }, apiKey) {
return withRetry(async () => {
const html = buildCertificateHtml({ name, course, date, certId });
const response = await fetch("https://pdf.funbrew.cloud/api/pdf/generate", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: { format: "A4", landscape: true, printBackground: true },
}),
signal: AbortSignal.timeout(120_000), // 120秒タイムアウト
});
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After") || "60";
throw new Error(`429 Too Many Requests — Retry-After: ${retryAfter}s`);
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return Buffer.from(await response.arrayBuffer());
});
}
function buildCertificateHtml({ name, course, date, certId }) {
return `
<div style="font-family: Georgia, serif; text-align: center; padding: 80px 60px; border: 8px double #b8860b;">
<h1 style="font-size: 28px; margin: 20px 0;">修了証</h1>
<p style="font-size: 22px; font-weight: 700; margin: 28px 0;">${name}</p>
<p style="font-size: 18px; margin: 20px 0;">${course}</p>
<p style="font-size: 12px; color: #9ca3af;">${date}</p>
<p style="font-size: 9px; color: #d1d5db; margin-top: 30px;">証明書番号: ${certId}</p>
</div>
`;
}
Python(tenacity ライブラリによるリトライ)
import os
import httpx
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
before_sleep_log,
)
import logging
logger = logging.getLogger(__name__)
class RateLimitError(Exception):
pass
@retry(
stop=stop_after_attempt(4),
wait=wait_exponential(multiplier=1, min=2, max=30),
retry=retry_if_exception_type((httpx.TransportError, RateLimitError)),
before_sleep=before_sleep_log(logger, logging.WARNING),
)
def generate_certificate_with_retry(name: str, course: str, date: str, cert_id: str) -> bytes:
"""リトライ付きで1件の証明書PDFを生成する"""
html = f"""
<div style="font-family: 'Noto Sans JP', sans-serif; text-align: center; padding: 80px 60px; border: 8px double #b8860b;">
<h1 style="font-size: 28px; margin: 20px 0;">修了証</h1>
<p style="font-size: 22px; font-weight: 700; margin: 28px 0;">{name}</p>
<p style="font-size: 18px; margin: 20px 0;">{course}</p>
<p style="font-size: 12px; color: #9ca3af;">{date}</p>
<p style="font-size: 9px; color: #d1d5db;">証明書番号: {cert_id}</p>
</div>
"""
response = httpx.post(
"https://pdf.funbrew.cloud/api/pdf/generate",
headers={"Authorization": f"Bearer {os.environ['FUNBREW_PDF_API_KEY']}"},
json={
"html": html,
"options": {"format": "A4", "landscape": True, "printBackground": True},
},
timeout=120,
)
if response.status_code == 429:
raise RateLimitError(f"Rate limited — Retry-After: {response.headers.get('Retry-After', '?')}s")
response.raise_for_status()
return response.content
レート制限対応
FUNBREW PDF APIにはプランごとのレート制限があります。大量バッチ発行では制限を超えないよう適切に制御する必要があります。
レート制限の仕組み
| プラン | 上限(リクエスト/分) | バースト許容 |
|---|---|---|
| Starter | 20 req/min | 短時間のバースト可 |
| Pro | 60 req/min | - |
| Enterprise | カスタム | - |
429レスポンスが返ったら Retry-After ヘッダーの秒数だけ待機して再試行するのが基本パターンです。詳細はPDF APIレート制限ガイドを参照してください。
セマフォで同時実行数を制御する(Node.js)
/**
* 並列実行数を制限するセマフォ実装
*/
function createSemaphore(limit) {
let count = 0;
const queue = [];
return {
async acquire() {
if (count < limit) {
count++;
return;
}
await new Promise((resolve) => queue.push(resolve));
count++;
},
release() {
count--;
if (queue.length > 0) queue.shift()();
},
};
}
async function batchGenerateCertificates(recipients, apiKey, outputDir = "certs") {
const fs = require("fs");
const path = require("path");
fs.mkdirSync(outputDir, { recursive: true });
const sem = createSemaphore(5); // 同時5件まで(Proプランの場合は10まで可)
const results = { success: [], failed: [] };
await Promise.allSettled(
recipients.map(async (r) => {
await sem.acquire();
try {
const pdf = await generateCertificateWithRetry(r, apiKey);
const filePath = path.join(outputDir, `cert-${r.certId}.pdf`);
fs.writeFileSync(filePath, pdf);
results.success.push(r.certId);
console.log(`[OK] ${r.certId}`);
} catch (err) {
results.failed.push({ certId: r.certId, error: err.message });
console.error(`[NG] ${r.certId}: ${err.message}`);
} finally {
sem.release();
}
})
);
console.log(`\n完了: ${results.success.length}件 / 失敗: ${results.failed.length}件`);
// 失敗分をJSONに保存(後で再処理するため)
if (results.failed.length > 0) {
fs.writeFileSync("failed-certs.json", JSON.stringify(results.failed, null, 2));
console.log("failed-certs.json に失敗IDを保存しました。再実行してください。");
}
return results;
}
実運用Tips
証明書テンプレート設計のベストプラクティス
テンプレートを本番環境で長期運用するために以下の点を意識してください。
1. 署名画像をBase64で埋め込む
外部URLへの参照はPDF生成時に読み込みタイムアウトを起こすことがあります。署名画像・ロゴ画像はBase64エンコードしてHTMLに埋め込むのが安全です。
import base64
def embed_image(image_path: str) -> str:
"""画像をBase64 data URIに変換する"""
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
ext = image_path.rsplit(".", 1)[-1]
return f"data:image/{ext};base64,{b64}"
# テンプレートに埋め込む
signature_data_uri = embed_image("signatures/director.png")
html = template.render(
name=recipient_name,
course=course_name,
date=completion_date,
cert_id=cert_id,
signature_uri=signature_data_uri,
)
<!-- テンプレート内での使用 -->
<img src="{{signature_uri}}" alt="署名" width="120" height="50"
style="display: block; margin: 10mm auto 0;">
2. QRコードを事前生成してBase64で埋め込む
import qrcode
import io
import base64
def generate_qr_data_uri(cert_id: str, verify_base_url: str) -> str:
"""証明書検証URLのQRコードをBase64 data URIで返す"""
url = f"{verify_base_url}/verify/{cert_id}"
qr = qrcode.QRCode(box_size=4, border=2)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buf = io.BytesIO()
img.save(buf, format="PNG")
b64 = base64.b64encode(buf.getvalue()).decode()
return f"data:image/png;base64,{b64}"
# 使用例
qr_uri = generate_qr_data_uri("CERT-2026-001234", "https://example.com")
3. フォントはFUNBREW PDFのプリインストールフォントを使う
外部フォントの読み込みはPDF生成時間を増加させます。FUNBREW PDFには日本語フォント(Noto Sans JP・Noto Serif JP)がプリインストールされているため、Google Fontsへのリンクは不要です。
<style>
body {
font-family: 'Noto Sans JP', 'Hiragino Kaku Gothic ProN', sans-serif;
}
.certificate-title {
font-family: 'Noto Serif JP', 'HiraMinProN-W3', serif;
}
</style>
バッチ発行後の検証チェックリスト
証明書を大量発行したあとは以下の検証ステップを実施してください。
import os
def validate_generated_pdfs(cert_dir: str, expected_ids: list[str]) -> dict:
"""生成済みPDFの検証を行う"""
results = {"ok": [], "missing": [], "corrupted": []}
for cert_id in expected_ids:
path = os.path.join(cert_dir, f"cert-{cert_id}.pdf")
if not os.path.exists(path):
results["missing"].append(cert_id)
continue
# PDFマジックバイト検証
with open(path, "rb") as f:
header = f.read(5)
if header != b"%PDF-":
results["corrupted"].append(cert_id)
elif os.path.getsize(path) < 1024: # 1KB未満は空ファイルの可能性
results["corrupted"].append(cert_id)
else:
results["ok"].append(cert_id)
print(f"OK: {len(results['ok'])} / 未生成: {len(results['missing'])} / 破損: {len(results['corrupted'])}")
return results
トラブルシューティング(FAQ)
Q: PDFが生成されない / 空のファイルになる
最も多い原因はAPIキーの未設定か誤設定です。
# 環境変数が設定されているか確認する
echo $FUNBREW_PDF_API_KEY
# curlで直接確認する(エラーメッセージが表示される)
curl -v -X POST https://pdf.funbrew.cloud/api/pdf/generate \
-H "Authorization: Bearer $FUNBREW_PDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "<h1>Test</h1>", "options": {"format": "A4"}}' \
-o test.pdf
401 Unauthorized が返ればキーの問題、200 OK でも test.pdf のサイズが0なら --output フラグの指定ミスです。
Q: 日本語が文字化けする
HTMLの <meta charset="UTF-8"> が抜けているケースが多いです。また、フォント指定に Noto Sans JP を含めてください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8"> <!-- 必須 -->
<style>
body { font-family: 'Noto Sans JP', sans-serif; }
</style>
</head>
詳しくは日本語PDF生成ガイドを参照してください。
Q: バッチ処理で一部が失敗する
一時的なネットワークエラーまたはレート制限が原因であることがほとんどです。
- 失敗したIDを
failed-certs.jsonに保存し、再実行する - 同時リクエスト数を5以下に制限する(セマフォパターン)
- Retry-Afterヘッダーを確認してレート制限に対応する
- タイムアウトを120秒以上に設定する(複雑なHTMLはレンダリングに時間がかかる場合がある)
Q: 証明書のデザインがずれる
印刷向けCSSで @page を明示的に設定してください。
<style>
@page {
size: A4 landscape;
margin: 0; /* 余白はコンテンツ内で制御する */
}
* {
box-sizing: border-box;
}
body {
width: 297mm;
height: 210mm;
margin: 0;
padding: 0;
}
</style>
CSSレンダリングの詳細はPDF向けCSSレイアウトガイドを参照してください。
Q: 証明書番号を毎回ユニークにするには
データベースのオートインクリメントIDとハッシュを組み合わせるか、UUIDを使います。
import hashlib
import uuid
from datetime import date
def generate_cert_id_v1(recipient_id: int, course_id: int) -> str:
"""受講者ID・コースID・日付のハッシュから証明書IDを生成する"""
today = date.today().strftime("%Y%m%d")
raw = f"{recipient_id}-{course_id}-{today}"
digest = hashlib.sha256(raw.encode()).hexdigest()[:10].upper()
return f"CERT-{today}-{digest}"
def generate_cert_id_v2() -> str:
"""UUIDベースの証明書ID(完全にランダム)"""
return f"CERT-{uuid.uuid4().hex[:12].upper()}"
SDKレベルのHMAC改ざん防止IDの実装は証明書PDF生成SDKガイドで詳しく解説しています。
Q: 大量の証明書を効率よく保存するには
生成直後にS3やGoogle Cloud Storageにアップロードし、ローカルには保存しないパターンが推奨です。
import boto3
def upload_to_s3(pdf_bytes: bytes, cert_id: str, bucket: str) -> str:
"""証明書PDFをS3にアップロードしてURLを返す"""
s3 = boto3.client("s3")
key = f"certificates/{cert_id}.pdf"
s3.put_object(
Bucket=bucket,
Key=key,
Body=pdf_bytes,
ContentType="application/pdf",
)
# 期限付きダウンロードURL(1時間)
return s3.generate_presigned_url(
"get_object",
Params={"Bucket": bucket, "Key": key},
ExpiresIn=3600,
)
S3保存と監査ログについての詳細は証明書PDF自動化ガイドの「保存・アーカイブのベストプラクティス」を参照してください。
まとめ
このガイドでカバーした主要ポイントを整理します。
- 最初の証明書: cURL・Node.js・Python・PHPで1リクエストから始める
- テンプレート管理: HTMLファイルを分離し、Jinja2・Handlebarsでデータを注入する
- レスポンス形式: バイナリモードで即時ダウンロード、URLモードでリンクを取得
- エラーハンドリング: 指数バックオフ付きリトライで一時エラーを吸収する
- レート制限対応: セマフォで同時実行数を制限し、429エラーに適切に対応する
- 実運用Tips: 画像はBase64で埋め込み、QRコードは事前生成し、プリインストールフォントを活用する
- トラブルシューティング: 文字化け・レイアウトずれ・バッチ失敗の対処法
証明書PDF生成を今すぐ試すにはFUNBREW PDF Playgroundが最も手軽です。APIを使い始めるにはAPIドキュメントを参照してください。大規模なバッチ発行のアーキテクチャはPDF一括生成ガイドで解説しています。イベント向けの一括発行パイプラインはイベント証明書一括発行ガイドを参照してください。