研修を修了した受講者に修了証を発行する、資格試験の合格者に証明書を送る、イベントの参加証を一括発行する――こうした証明書PDFの発行業務は、手作業で行うと担当者の負荷が高く、記載ミスのリスクもあります。
PDF APIを使えば、データベースの情報をHTMLテンプレートに差し込んでPDFを生成するプロセスを完全自動化できます。この記事では、証明書PDF自動生成の設計から実装まで、FUNBREW PDFを用いた実践的な手順を解説します。
バッチ発行の仕組みはPDF一括生成ガイドも参照してください。テンプレートエンジンの詳細はPDFテンプレートエンジン活用ガイドで解説しています。PDF APIに初めて触れる方は言語別クイックスタートで最初のリクエストを試してみてください。PDF APIの全体像はHTML→PDF変換 完全ガイドで把握できます。Markdownで証明書テンプレートを管理したい場合はMarkdown→PDF APIガイドも参照してください。
証明書PDF自動生成のユースケース
修了証(Completion Certificate)
社内研修・eラーニング・オンラインコースで受講者全員に自動発行。
- 受講者名・コース名・修了日を動的に差し込み
- スコア・評価結果を条件分岐で出力
- QRコードで真正性を検証可能にする
資格証明書(Qualification Certificate)
試験合格者や認定者への証明書発行を自動化。
- 有効期限の自動計算
- 証明書番号の採番・管理
- 更新証明の差分生成
参加証・表彰状(Participation / Award Certificate)
イベント・セミナー・コンテストの参加者や受賞者へ一括発行。
- イベント名・日時・場所の動的挿入
- 受賞部門・順位に応じたデザイン切り替え
テンプレート設計のポイント
HTMLテンプレートの基本構造
証明書デザインはHTMLとCSSで完全に制御できます。印刷向けCSSを活用してA4/レター判に最適化します。CSS設計の詳細はPDF出力向けCSSレイアウトのコツも参考にしてください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
@page {
size: A4 landscape;
margin: 0;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
width: 297mm;
height: 210mm;
font-family: 'Noto Sans JP', 'Hiragino Kaku Gothic ProN', sans-serif;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.certificate {
width: 270mm;
height: 185mm;
border: 4px double #2c5282;
padding: 20mm 25mm;
text-align: center;
position: relative;
}
.certificate-title {
font-size: 36pt;
font-weight: bold;
color: #2c5282;
margin-bottom: 10mm;
letter-spacing: 0.1em;
}
.recipient-name {
font-size: 28pt;
border-bottom: 2px solid #2c5282;
padding-bottom: 3mm;
margin: 8mm auto;
display: inline-block;
min-width: 150mm;
}
.course-name {
font-size: 16pt;
color: #4a5568;
margin: 5mm 0;
}
.completion-date {
font-size: 12pt;
color: #718096;
margin-top: 8mm;
}
.certificate-id {
position: absolute;
bottom: 8mm;
right: 12mm;
font-size: 8pt;
color: #a0aec0;
}
</style>
</head>
<body>
<div class="certificate">
<div class="certificate-title">修了証</div>
<div class="course-name">{{courseName}}</div>
<div class="recipient-name">{{recipientName}} 様</div>
<p style="font-size:13pt; margin-top:6mm; line-height:1.8;">
あなたは上記のコースを優秀な成績で修了されましたので、<br>
ここに証明いたします。
</p>
<div class="completion-date">
修了日: {{completionDate}}
</div>
<div class="certificate-id">証明書番号: {{certificateId}}</div>
</div>
</body>
</html>
データバインディング
テンプレート変数({{変数名}})を実際のデータで置換してからAPIに渡します。
# Python + Jinja2 の例
from jinja2 import Template
template_str = open("certificate.html").read()
template = Template(template_str)
html = template.render(
recipientName="田中 太郎",
courseName="Python実践講座",
completionDate="2026年3月31日",
certificateId="CERT-2026-001234",
)
// Node.js + Handlebars の例
const Handlebars = require('handlebars');
const fs = require('fs');
const templateStr = fs.readFileSync('certificate.html', 'utf-8');
const template = Handlebars.compile(templateStr);
const html = template({
recipientName: '田中 太郎',
courseName: 'Python実践講座',
completionDate: '2026年3月31日',
certificateId: 'CERT-2026-001234',
});
コード例:単一証明書の生成
curl
# HTMLをBase64エンコードして送信
HTML_CONTENT='<html>...(証明書HTML)...</html>'
curl -X POST https://pdf.funbrew.cloud/api/v1/generate \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"html\": $(echo "$HTML_CONTENT" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))'),
\"options\": {
\"format\": \"A4\",
\"landscape\": true,
\"printBackground\": true,
\"margin\": {\"top\": \"0\", \"right\": \"0\", \"bottom\": \"0\", \"left\": \"0\"}
}
}" \
--output "certificate-001.pdf"
Python(単一生成)
import requests
from jinja2 import Template
def generate_certificate(
recipient_name: str,
course_name: str,
completion_date: str,
certificate_id: str,
api_key: str,
) -> bytes:
"""1件の証明書PDFを生成する"""
template_str = open("certificate.html").read()
html = Template(template_str).render(
recipientName=recipient_name,
courseName=course_name,
completionDate=completion_date,
certificateId=certificate_id,
)
response = requests.post(
"https://pdf.funbrew.cloud/api/v1/generate",
headers={"Authorization": f"Bearer {api_key}"},
json={
"html": html,
"options": {
"format": "A4",
"landscape": True,
"printBackground": True,
"margin": {"top": "0", "right": "0", "bottom": "0", "left": "0"},
},
},
timeout=120,
)
response.raise_for_status()
return response.content
# 使用例
pdf = generate_certificate(
recipient_name="田中 太郎",
course_name="Python実践講座",
completion_date="2026年3月31日",
certificate_id="CERT-2026-001234",
api_key="your-api-key",
)
with open("certificate-tanaka.pdf", "wb") as f:
f.write(pdf)
Node.js(単一生成)
const axios = require('axios');
const Handlebars = require('handlebars');
const fs = require('fs');
async function generateCertificate({ recipientName, courseName, completionDate, certificateId }, apiKey) {
const templateStr = fs.readFileSync('certificate.html', 'utf-8');
const html = Handlebars.compile(templateStr)({
recipientName,
courseName,
completionDate,
certificateId,
});
const response = await axios.post(
'https://pdf.funbrew.cloud/api/v1/generate',
{
html,
options: {
format: 'A4',
landscape: true,
printBackground: true,
margin: { top: '0', right: '0', bottom: '0', left: '0' },
},
},
{
headers: { Authorization: `Bearer ${apiKey}` },
responseType: 'arraybuffer',
timeout: 120000,
}
);
return Buffer.from(response.data);
}
バッチ生成:複数証明書の一括発行
研修の修了者が100名いる場合、1件ずつAPIを呼ぶより並行処理で一括生成する方が効率的です。請求書など他のドキュメントとの比較は請求書PDF自動化ガイドを参照してください。大規模なバッチ生成のアーキテクチャについては大量PDF一括生成ガイドで詳しく解説しています。
Python(バッチ生成)
import asyncio
import aiohttp
from jinja2 import Template
from pathlib import Path
async def generate_certificate_async(
session: aiohttp.ClientSession,
recipient: dict,
template: Template,
api_key: str,
output_dir: Path,
) -> dict:
"""非同期で1件の証明書を生成する"""
html = template.render(**recipient)
try:
async with session.post(
"https://pdf.funbrew.cloud/api/v1/generate",
headers={"Authorization": f"Bearer {api_key}"},
json={
"html": html,
"options": {
"format": "A4",
"landscape": True,
"printBackground": True,
},
},
timeout=aiohttp.ClientTimeout(total=120),
) as response:
if response.status == 200:
pdf_bytes = await response.read()
filename = output_dir / f"cert-{recipient['certificateId']}.pdf"
filename.write_bytes(pdf_bytes)
return {"id": recipient["certificateId"], "status": "success"}
else:
return {
"id": recipient["certificateId"],
"status": "failed",
"error": f"HTTP {response.status}",
}
except Exception as e:
return {"id": recipient["certificateId"], "status": "failed", "error": str(e)}
async def batch_generate_certificates(recipients: list[dict], api_key: str, output_dir: str = "certs"):
"""受講者リストから証明書を一括生成する"""
template_str = open("certificate.html").read()
template = Template(template_str)
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
# 同時リクエスト数を制限(レート制限対策)
semaphore = asyncio.Semaphore(5)
async def limited_generate(session, recipient):
async with semaphore:
return await generate_certificate_async(session, recipient, template, api_key, output_path)
async with aiohttp.ClientSession() as session:
tasks = [limited_generate(session, r) for r in recipients]
results = await asyncio.gather(*tasks)
success = [r for r in results if r["status"] == "success"]
failed = [r for r in results if r["status"] == "failed"]
print(f"完了: {len(success)}件 / 失敗: {len(failed)}件")
return results
# 使用例
recipients = [
{
"recipientName": "田中 太郎",
"courseName": "Python実践講座",
"completionDate": "2026年3月31日",
"certificateId": "CERT-2026-001",
},
{
"recipientName": "鈴木 花子",
"courseName": "Python実践講座",
"completionDate": "2026年3月31日",
"certificateId": "CERT-2026-002",
},
# ... 残りの受講者
]
asyncio.run(batch_generate_certificates(recipients, api_key="your-api-key"))
Node.js(バッチ生成)
const axios = require('axios');
const Handlebars = require('handlebars');
const fs = require('fs');
const path = require('path');
// 同時実行数を制限するセマフォ
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 templateStr = fs.readFileSync('certificate.html', 'utf-8');
const template = Handlebars.compile(templateStr);
fs.mkdirSync(outputDir, { recursive: true });
const semaphore = createSemaphore(5); // 同時5件まで
const results = await Promise.allSettled(
recipients.map(async (recipient) => {
await semaphore.acquire();
try {
const html = template(recipient);
const response = await axios.post(
'https://pdf.funbrew.cloud/api/v1/generate',
{ html, options: { format: 'A4', landscape: true, printBackground: true } },
{
headers: { Authorization: `Bearer ${apiKey}` },
responseType: 'arraybuffer',
timeout: 120000,
}
);
const filePath = path.join(outputDir, `cert-${recipient.certificateId}.pdf`);
fs.writeFileSync(filePath, Buffer.from(response.data));
return { id: recipient.certificateId, status: 'success' };
} catch (err) {
return { id: recipient.certificateId, status: 'failed', error: err.message };
} finally {
semaphore.release();
}
})
);
const succeeded = results.filter((r) => r.value?.status === 'success').length;
const failed = results.filter((r) => r.value?.status === 'failed').length;
console.log(`完了: ${succeeded}件 / 失敗: ${failed}件`);
return results;
}
証明書番号の管理と真正性検証
証明書番号の採番
import hashlib
import datetime
def generate_certificate_id(recipient_id: str, course_id: str) -> str:
"""受講者IDとコースIDから証明書番号を生成する"""
date_str = datetime.date.today().strftime("%Y%m%d")
hash_input = f"{recipient_id}-{course_id}-{date_str}"
short_hash = hashlib.sha256(hash_input.encode()).hexdigest()[:8].upper()
return f"CERT-{date_str}-{short_hash}"
# 例: CERT-20260331-A3F7B2C1
QRコードで真正性を検証
証明書にQRコードを埋め込み、URLで真正性を確認できます。
<!-- qrcode.jsを使う例 -->
<div id="qr-container">
<canvas id="qr-code"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<script>
QRCode.toCanvas(
document.getElementById('qr-code'),
'https://example.com/verify/{{certificateId}}',
{ width: 80 }
);
</script>
実際の証明書自動生成の活用事例はユースケース一覧もご覧ください。請求書の自動化事例は/use-cases/invoices、レポート生成は/use-cases/reportsも参考になります。Next.js/Nuxtアプリから証明書を生成する場合はNext.js・Nuxt PDF APIガイド、定期レポートの自動生成はレポートPDF生成ガイドもあわせてご覧ください。
デジタル署名と押印画像の埋め込み
多くの証明書ワークフローでは、講師のサイン・責任者の署名・公印の画像が必要です。HTMLテンプレートに署名画像を直接埋め込むことで、完全にプログラマブルな署名付き証明書を実現できます。
署名画像のテンプレート埋め込み
<div class="signatures" style="display: flex; justify-content: space-around; margin-top: 15mm;">
<div class="signer" style="text-align: center;">
<img src="data:image/png;base64,{{signatureB64}}"
alt="署名" width="120" height="60"
style="display: block; margin: 0 auto;">
<div style="border-top: 1px solid #333; padding-top: 2mm; font-size: 10pt;">
{{signerName}}<br>
<span style="font-size: 8pt; color: #718096;">{{signerTitle}}</span>
</div>
</div>
<div class="stamp" style="text-align: center;">
<img src="data:image/png;base64,{{stampB64}}"
alt="公印" width="80" height="80">
</div>
</div>
サーバーサイドでの署名データ生成
import base64
def load_signature(signer_id: str) -> dict:
"""署名者の画像とメタデータを読み込む"""
sig_path = f"signatures/{signer_id}.png"
with open(sig_path, "rb") as f:
sig_b64 = base64.b64encode(f.read()).decode()
return {
"signatureB64": sig_b64,
"signerName": "田中 教授",
"signerTitle": "教育部門 ディレクター",
}
# テンプレートコンテキストに署名データをマージ
recipient_data = {**recipient, **load_signature("tanaka")}
html = template.render(**recipient_data)
const fs = require('fs');
function loadSignature(signerId) {
const sigB64 = fs.readFileSync(`signatures/${signerId}.png`).toString('base64');
return {
signatureB64: sigB64,
signerName: '田中 教授',
signerTitle: '教育部門 ディレクター',
};
}
const data = { ...recipient, ...loadSignature('tanaka') };
const html = template(data);
メール配信:証明書を受講者に直接送信
PDF生成後の次のステップは、通常メールでの配信です。PDF生成とメールサービスを組み合わせて、完全自動化パイプラインを構築できます。
Python + SMTP
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.mime.text import MIMEText
def send_certificate_email(
recipient_email: str,
recipient_name: str,
pdf_bytes: bytes,
certificate_id: str,
smtp_config: dict,
):
"""証明書PDFをメール添付で送信する"""
msg = MIMEMultipart()
msg["From"] = smtp_config["from"]
msg["To"] = recipient_email
msg["Subject"] = f"修了証のお知らせ — {certificate_id}"
body = f"{recipient_name} 様\n\n修了証を添付いたします。\n\nよろしくお願いいたします。"
msg.attach(MIMEText(body, "plain", "utf-8"))
attachment = MIMEApplication(pdf_bytes, _subtype="pdf")
attachment.add_header("Content-Disposition", "attachment", filename=f"{certificate_id}.pdf")
msg.attach(attachment)
with smtplib.SMTP(smtp_config["host"], smtp_config["port"]) as server:
server.starttls()
server.login(smtp_config["user"], smtp_config["password"])
server.send_message(msg)
Node.js + Nodemailer
const nodemailer = require('nodemailer');
async function sendCertificateEmail(recipientEmail, recipientName, pdfBuffer, certificateId) {
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});
await transporter.sendMail({
from: process.env.FROM_EMAIL,
to: recipientEmail,
subject: `修了証のお知らせ — ${certificateId}`,
text: `${recipientName} 様\n\n修了証を添付いたします。\n\nよろしくお願いいたします。`,
attachments: [{ filename: `${certificateId}.pdf`, content: pdfBuffer }],
});
}
生成+メール送信の統合パイプライン
async def generate_and_send(recipients, api_key, smtp_config):
"""証明書を生成してメール送信する統合パイプライン"""
results = await batch_generate_certificates(recipients, api_key)
for recipient, result in zip(recipients, results):
if result["status"] == "success":
pdf_path = f"certs/cert-{recipient['certificateId']}.pdf"
pdf_bytes = open(pdf_path, "rb").read()
send_certificate_email(
recipient_email=recipient["email"],
recipient_name=recipient["recipientName"],
pdf_bytes=pdf_bytes,
certificate_id=recipient["certificateId"],
smtp_config=smtp_config,
)
print(f"送信完了: {recipient['email']}")
保存・アーカイブのベストプラクティス
証明書PDFはコンプライアンス・監査証跡・受講者の再ダウンロードのために保存が必要です。推奨パターンを紹介します。
S3へのクラウドストレージ保存
import boto3
def archive_certificate(pdf_bytes: bytes, certificate_id: str, metadata: dict) -> str:
"""証明書PDFをS3にアップロードしてURLを返す"""
s3 = boto3.client("s3")
key = f"certificates/{certificate_id}.pdf"
s3.put_object(
Bucket="my-certificates-bucket",
Key=key,
Body=pdf_bytes,
ContentType="application/pdf",
Metadata={
"recipient": metadata["recipientName"],
"course": metadata["courseName"],
"issued": metadata["completionDate"],
},
)
return f"s3://my-certificates-bucket/{key}"
監査証跡のためのデータベース記録
# 生成・アーカイブ後に発行記録を残す
def record_issuance(db, certificate_id: str, recipient: dict, s3_url: str):
db.execute(
"""INSERT INTO certificate_records
(certificate_id, recipient_name, recipient_email, course_name, issued_at, s3_url)
VALUES (?, ?, ?, ?, NOW(), ?)""",
(certificate_id, recipient["recipientName"], recipient["email"],
recipient["courseName"], s3_url),
)
期限付きダウンロードURL(署名付きURL)
def get_download_url(certificate_id: str, expires_in: int = 3600) -> str:
"""期限付きダウンロードURLを生成する(デフォルト1時間)"""
s3 = boto3.client("s3")
return s3.generate_presigned_url(
"get_object",
Params={"Bucket": "my-certificates-bucket", "Key": f"certificates/{certificate_id}.pdf"},
ExpiresIn=expires_in,
)
エラーハンドリングとリトライ
バッチ発行では一部のリクエストが失敗することがあります。リトライ戦略についてはPDF APIエラーハンドリングガイドを参照してください。失敗したIDだけを再処理するパターンを実装することで、全量再実行を避けられます。Webhookを使って失敗を即時検知する方法はWebhook連携ガイドで解説しています。
# 失敗分だけ再試行
failed_recipients = [r for r in recipients if r["certificateId"] in failed_ids]
asyncio.run(batch_generate_certificates(failed_recipients, api_key="your-api-key"))
APIキーのセキュリティ
証明書生成は機密性の高い業務です。APIキーの安全な管理についてはPDF APIセキュリティガイドを必ず確認してください。本番環境では環境変数でキーを管理し、ログにキーが出力されないよう注意します。
まとめ
証明書PDF自動生成の実装ポイントを整理します。
- テンプレート設計: Handlebars / Jinja2 でHTMLテンプレートを作成し、変数を差し込む
- デザイン最適化:
@pageと印刷向けCSSで用紙サイズ・余白を制御する - バッチ処理: セマフォで同時リクエスト数を制限しながら並行生成する
- 証明書番号: ハッシュベースのIDで一意性を保証し、QRコードで検証可能にする
- エラー対応: 失敗分を記録して再処理できる仕組みを用意する
FUNBREW PDFのプレイグラウンドでテンプレートHTMLを貼り付けて動作を確認してから本番実装に進むことをおすすめします。APIの詳細はドキュメントを参照してください。利用量に応じた適切なプランの選択は料金プラン比較が参考になります。サーバーレス環境での証明書発行はサーバーレスPDF生成ガイド、Ruby/Goでの実装はRuby/Goクイックスタートも参考にしてください。Django/FastAPIでの証明書生成はDjango/FastAPIガイド、Docker環境での構築はDocker & Kubernetesガイド、本番運用全般は本番運用チェックリストをご覧ください。他のPDF APIとの比較はPDF API比較も参考になります。