Stripe 결제 시 PDF 인보이스 자동 생성 방법

발행: (2026년 2월 26일 오후 05:17 GMT+9)
5 분 소요
원문: Dev.to

Source: Dev.to

Stripe는 영수증을 자동으로 생성하지만 Stripe 브랜드가 붙어 있어 맞춤 설정이 불가능합니다. 브랜드에 맞는 인보이스가 필요하거나, 맞춤 라인 아이템을 포함해야 하거나, 특정 국가에서 부가가치세(VAT) 규정 준수를 위해 필요하다면 직접 생성해야 합니다.

전체 파이프라인: Stripe webhook → render HTML template → capture as PDF → email to customer.

설정

다음 세 가지가 필요합니다:

  1. Stripe 웹훅payment_intent.succeeded 또는 invoice.payment_succeeded 이벤트에서 트리거됩니다.
  2. HTML 인보이스 템플릿(아래 참조).
  3. PageBolt 호출 하나로 렌더링된 HTML을 PDF로 캡처합니다.

필요한 npm 패키지를 설치합니다:

npm install stripe @sendgrid/mail express

웹훅 핸들러

import Stripe from "stripe";
import sgMail from "@sendgrid/mail";
import express from "express";
import { renderInvoiceHtml } from "./templates/invoice.js";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
sgMail.setApiKey(process.env.SENDGRID_API_KEY);

const app = express();

// Use raw body for Stripe signature verification
app.post(
  "/webhooks/stripe",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const sig = req.headers["stripe-signature"];

    let event;
    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        sig,
        process.env.STRIPE_WEBHOOK_SECRET
      );
    } catch (err) {
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    if (event.type === "invoice.payment_succeeded") {
      await handleInvoicePaid(event.data.object);
    }

    res.json({ received: true });
  }
);

PDF 생성 및 전송

async function handleInvoicePaid(stripeInvoice) {
  // Fetch full customer details
  const customer = await stripe.customers.retrieve(stripeInvoice.customer);

  // Build your invoice data
  const invoiceData = {
    number: stripeInvoice.number,
    date: new Date(stripeInvoice.created * 1000).toLocaleDateString(),
    customerName: customer.name,
    customerEmail: customer.email,
    lines: stripeInvoice.lines.data.map((line) => ({
      description: line.description,
      amount: (line.amount / 100).toFixed(2),
      currency: stripeInvoice.currency.toUpperCase(),
    })),
    total: (stripeInvoice.amount_paid / 100).toFixed(2),
    currency: stripeInvoice.currency.toUpperCase(),
  };

  // Render HTML template
  const html = renderInvoiceHtml(invoiceData);

  // Capture as PDF via PageBolt
  const pdfRes = await fetch("https://pagebolt.dev/api/v1/pdf", {
    method: "POST",
    headers: {
      "x-api-key": process.env.PAGEBOLT_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ html }),
  });

  const pdfBuffer = Buffer.from(await pdfRes.arrayBuffer());

  // Email to customer
  await sgMail.send({
    to: customer.email,
    from: "billing@yourapp.com",
    subject: `Invoice ${invoiceData.number} — Payment confirmed`,
    text: `Hi ${customer.name}, your payment was successful. Invoice attached.`,
    attachments: [
      {
        content: pdfBuffer.toString("base64"),
        filename: `invoice-${invoiceData.number}.pdf`,
        type: "application/pdf",
        disposition: "attachment",
      },
    ],
  });

  console.log(`Invoice ${invoiceData.number} sent to ${customer.email}`);
}

HTML 인보이스 템플릿

// templates/invoice.js
export function renderInvoiceHtml(invoice) {
  const lineItems = invoice.lines
    .map(
      (line) => `
        ${line.description}
        ${line.currency} ${line.amount}
      `
    )
    .join("");

  return `
    <style>
      body { font-family: -apple-system, sans-serif; color: #111; max-width: 680px; margin: 40px auto; padding: 0 20px; }
      .header { display: flex; justify-content: space-between; margin-bottom: 40px; }
      .logo { font-size: 24px; font-weight: 700; }
      table { width: 100%; border-collapse: collapse; margin: 24px 0; }
      th { text-align: left; border-bottom: 2px solid #111; padding: 8px 0; }
      td { padding: 10px 0; border-bottom: 1px solid #eee; }
      .total { font-size: 18px; font-weight: 700; text-align: right; margin-top: 16px; }
      .meta { color: #666; font-size: 14px; }
    </style>

    <div class="header">
      <div class="logo">YourApp</div>
      <div>
        <div>Invoice ${invoice.number}</div>
        <div>${invoice.date}</div>
      </div>
    </div>

    <div>
      <strong>Bill to:</strong><br/>
      ${invoice.customerName}<br/>
      ${invoice.customerEmail}
    </div>

    <table>
      <thead>
        <tr>
          <th>Description</th>
          <th>Amount</th>
        </tr>
      </thead>
      <tbody>
        ${lineItems}
      </tbody>
    </table>

    <div class="total">Total: ${invoice.currency} ${invoice.total}</div>
  `;
}

일회성 결제도 처리하기

payment_intent.succeeded (구독이 아닌 결제) 에 대해 간단한 영수증을 생성할 수 있습니다:

if (event.type === "payment_intent.succeeded") {
  const paymentIntent = event.data.object;

  // Retrieve the charge to get receipt details
  const charges = await stripe.charges.list({
    payment_intent: paymentIntent.id,
    limit: 1,
  });

  const charge = charges.data[0];
  if (!charge?.billing_details?.email) return;

  const html = renderSimpleReceiptHtml({
    amount: (paymentIntent.amount / 100).toFixed(2),
    currency: paymentIntent.currency.toUpperCase(),
    // add any other fields you need
  });

  // …capture PDF and email as shown above
}

(renderSimpleReceiptHtml을 인보이스 템플릿과 유사하게 구현하세요.)

PDF 인보이스 생성 및 이메일 전송 (일회성 결제)

// Example continuation for one‑time payments
const html = renderSimpleReceiptHtml({
  amount: (paymentIntent.amount / 100).toFixed(2),
  currency: paymentIntent.currency.toUpperCase(),
  email: charge.billing_details.email,
  date: new Date().toLocaleDateString(),
  description: paymentIntent.description || "Payment",
});

const pdfRes = await fetch("https://pagebolt.dev/api/v1/pdf", {
  method: "POST",
  headers: {
    "x-api-key": process.env.PAGEBOLT_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ html }),
});

const pdfBuffer = Buffer.from(await pdfRes.arrayBuffer());

// Email as shown in the invoice example
await sgMail.send({
  to: charge.billing_details.email,
  from: "billing@yourapp.com",
  subject: "Payment receipt",
  text: "Your payment was successful. Receipt attached.",
  attachments: [
    {
      content: pdfBuffer.toString("base64"),
      filename: "receipt.pdf",
      type: "application/pdf",
      disposition: "attachment",
    },
  ],
});

S3에 PDF 저장 (선택 사항)

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

const s3 = new S3Client({ region: "us-east-1" });

async function storePdf(pdfBuffer, invoiceNumber) {
  await s3.send(
    new PutObjectCommand({
      Bucket: process.env.S3_BUCKET,
      Key: `invoices/${invoiceNumber}.pdf`,
      Body: pdfBuffer,
      ContentType: "application/pdf",
    })
  );

  return `https://${process.env.S3_BUCKET}.s3.amazonaws.com/invoices/${invoiceNumber}.pdf`;
}

Stripe CLI로 로컬 테스트

# Forward webhooks to your local server
stripe listen --forward-to localhost:3000/webhooks/stripe

# Trigger a test payment
stripe trigger invoice.payment_succeeded

PDF는 테스트 이벤트가 발생한 후 몇 초 안에 생성되어 이메일로 전송됩니다.

무료로 사용해 보세요 — 월 100회 요청, 신용카드 필요 없음. → 2분 안에 시작하기

0 조회
Back to Blog

관련 글

더 보기 »