이 글은 누구를 위한 것인가
- B2B 주문이 늘면서 세금계산서 발행이 수동으로 힘든 팀
- 국세청 API 연동 방법을 모르는 개발자
- 월말 세금계산서 일괄 발행으로 CS가 쌓이는 운영팀
들어가며
B2B 이커머스에서 세금계산서는 필수다. 수동 발행은 오류가 많고 인력이 많이 든다. 국세청 전자세금계산서 체계를 연동하면 주문 완료 즉시 자동 발행이 가능하다. 발행·전송 절차와 서식 기준은 홈택스의 전자세금계산서 안내가 1차 출처다.
이 글은 bluefoxdev.kr의 B2B 이커머스 시스템 설계 를 참고하여 작성했습니다.
1. 전자세금계산서 발행 흐름
[세금계산서 자동 발행 흐름]
주문 완료
↓
사업자 정보 확인
↓ (사업자 주문인 경우)
사업자번호 유효성 검증 (국세청 API)
↓
전자세금계산서 발행 (공급자 → 공급받는자)
↓
국세청 전송 (발행일 익일 17시까지)
↓
이메일 발송 (고객 + 사본 보관)
↓
회계 시스템 연동 (ERP)
[세금계산서 DB 스키마]
CREATE TABLE tax_invoices (
id UUID PRIMARY KEY,
order_id UUID NOT NULL,
invoice_number VARCHAR(24) UNIQUE, -- 일련번호 24자리
supplier_biz_num VARCHAR(10) NOT NULL, -- 공급자 사업자번호
buyer_biz_num VARCHAR(10) NOT NULL, -- 공급받는자 사업자번호
issue_date DATE NOT NULL,
supply_amount BIGINT NOT NULL, -- 공급가액
tax_amount BIGINT NOT NULL, -- 세액
total_amount BIGINT NOT NULL, -- 합계금액
status VARCHAR(20) DEFAULT 'issued', -- issued, sent, cancelled, amended
nts_id VARCHAR(50), -- 국세청 승인번호
sent_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
2. 국세청 API 연동
import httpx
from datetime import date
import hashlib
import hmac
class NTSInvoiceClient:
"""국세청 전자세금계산서 API 클라이언트"""
BASE_URL = "https://api.hometax.go.kr/etax"
def __init__(self, cert_path: str, cert_password: str):
self.cert_path = cert_path
self.cert_password = cert_password
async def validate_business_number(self, biz_num: str) -> dict:
"""사업자번호 유효성 검증"""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.BASE_URL}/validate/business",
json={"business_number": biz_num},
headers=self._get_auth_headers(),
)
return response.json()
async def issue_invoice(self, invoice_data: dict) -> dict:
"""전자세금계산서 발행"""
payload = {
"invoiceType": "01", # 일반 세금계산서
"issueDate": invoice_data["issue_date"],
"supplier": {
"businessNumber": invoice_data["supplier_biz_num"],
"businessName": invoice_data["supplier_name"],
"ceoName": invoice_data["supplier_ceo"],
"email": invoice_data["supplier_email"],
},
"buyer": {
"businessNumber": invoice_data["buyer_biz_num"],
"businessName": invoice_data["buyer_name"],
"email": invoice_data["buyer_email"],
},
"items": [
{
"name": item["name"],
"quantity": item["quantity"],
"unitPrice": item["unit_price"],
"supplyAmount": item["supply_amount"],
"taxAmount": item["tax_amount"],
}
for item in invoice_data["items"]
],
"totalSupplyAmount": invoice_data["supply_amount"],
"totalTaxAmount": invoice_data["tax_amount"],
"totalAmount": invoice_data["total_amount"],
}
async with httpx.AsyncClient(cert=(self.cert_path, self.cert_password)) as client:
response = await client.post(
f"{self.BASE_URL}/issue",
json=payload,
)
return response.json()
def _get_auth_headers(self) -> dict:
timestamp = str(int(datetime.now().timestamp()))
return {
"X-NTS-Timestamp": timestamp,
"X-NTS-Signature": self._sign(timestamp),
}
async def auto_issue_tax_invoice(order_id: str):
"""주문 완료 시 세금계산서 자동 발행"""
order = await get_order_with_buyer_info(order_id)
# 사업자 주문이 아니면 스킵
if not order.get("buyer_biz_num"):
return
# 사업자번호 검증
nts_client = NTSInvoiceClient(CERT_PATH, CERT_PASSWORD)
validation = await nts_client.validate_business_number(order["buyer_biz_num"])
if not validation["valid"]:
await notify_ops_team(f"유효하지 않은 사업자번호: {order['buyer_biz_num']}")
return
# 세금 계산
supply_amount = round(order["total_amount"] / 1.1) # VAT 역산
tax_amount = order["total_amount"] - supply_amount
invoice_data = {
"issue_date": date.today().isoformat(),
"supplier_biz_num": MY_BUSINESS_NUMBER,
"buyer_biz_num": order["buyer_biz_num"],
"buyer_name": order["company_name"],
"buyer_email": order["billing_email"],
"supply_amount": supply_amount,
"tax_amount": tax_amount,
"total_amount": order["total_amount"],
"items": build_invoice_items(order["items"]),
}
result = await nts_client.issue_invoice(invoice_data)
# DB 저장
await save_tax_invoice(order_id, result)
# 이메일 발송
await send_invoice_email(order["billing_email"], result["invoice_pdf_url"])
3. 수정 세금계산서 처리
async def issue_amended_invoice(original_invoice_id: str, reason: str):
"""수정 세금계산서 발행 (환불, 취소 시)"""
original = await get_tax_invoice(original_invoice_id)
# 음수 금액으로 수정 계산서 발행
amended_data = {
**original,
"invoiceType": "02", # 수정 세금계산서
"originalInvoiceId": original["nts_id"],
"amendReason": reason,
"supply_amount": -original["supply_amount"],
"tax_amount": -original["tax_amount"],
"total_amount": -original["total_amount"],
}
nts_client = NTSInvoiceClient(CERT_PATH, CERT_PASSWORD)
result = await nts_client.issue_invoice(amended_data)
# 원본 계산서 상태 업데이트
await db.execute(
"UPDATE tax_invoices SET status='amended' WHERE id=$1",
original_invoice_id
)
return result
마무리
전자세금계산서 자동화의 핵심은 주문 완료 직후 즉시 발행이다. 월말에 몰아서 처리하면 오류가 생기고, 고객도 불편하다. 사업자번호 검증을 주문 단계에서 하면 발행 실패를 사전에 막을 수 있다.