green snake
Photo by Pixabay on Pexels.com
目次

FastAPIで作るCS向け問い合わせ対応API設計入門:チケット管理・ステータス遷移・権限・監査ログまで実務で使える形に整える


要約

  • CS向け問い合わせ対応APIは、単なる「お問い合わせフォームの保存先」ではありません。実務では、問い合わせの受付、担当者アサイン、返信履歴、ステータス管理、内部メモ、エスカレーション、監査ログまで含めた業務基盤になります。
  • FastAPIでは、Depends による認証・権限管理、HTTPException による明確なエラー返却、BackgroundTasks による軽い通知処理、ミドルウェアによるリクエストID付与などを組み合わせることで、CS運用に必要なAPIを整理しやすくなります。
  • 問い合わせ対応APIで重要なのは、「誰が見られるか」「誰が返信できるか」「誰がステータスを変えられるか」を明確にすることです。顧客情報や会話履歴を扱うため、通常のCRUDより慎重な認可設計が必要です。
  • ステータス遷移は、openpendingwaiting_customerresolvedclosed のように状態を決め、許可された遷移だけをサービス層で制御すると安全です。
  • 本記事では、FastAPIでCS向け問い合わせ対応APIを作るときの基本設計を、データモデル、ルーター、検索、返信、内部メモ、監査ログ、通知、テストまで段階的に整理します。

誰が読んで得をするか

個人開発・学習者さん

自作サービスに「お問い合わせ機能」を付けたい方に向いています。最初は、名前・メール・本文を保存して終わりでも十分に見えるかもしれません。けれど、問い合わせが増えてくると、「対応済みか分からない」「誰が返信したか分からない」「過去のやり取りを追えない」といった問題が出てきます。

この記事では、最小の問い合わせ受付から、CS担当者が使いやすいチケット管理APIへ育てる道筋を紹介します。FastAPIの依存関係システムやステータスコード管理を使いながら、無理なく実務寄りの形へ近づけます。

小規模チームのバックエンドエンジニアさん

CS担当者、営業、開発者が同じ問い合わせを見ながら対応するような環境に向いています。たとえば、「CSが一次対応し、技術的なものだけ開発へエスカレーションする」「請求に関するものは経理へ回す」といった運用です。

この段階では、単なる問い合わせ一覧では足りません。担当者、カテゴリ、優先度、ステータス、内部メモ、監査ログが必要になります。この記事では、それらをFastAPIのAPI設計としてどう分けるかを具体的に整理します。

SaaS開発チーム・スタートアップの皆さま

複数テナント、複数プラン、社内管理画面、監査ログ、権限管理がすでに存在するSaaSチームに向いています。問い合わせ対応は、顧客体験と解約率に直結しやすい領域です。

問い合わせAPIを業務基盤として整えておくと、CSの対応品質が上がるだけでなく、問い合わせ分類、対応時間、未解決件数、エスカレーション率などのメトリクスも取りやすくなります。将来的にAI要約や自動分類を入れる場合にも、会話履歴と状態遷移が整理されていることが大切です。


アクセシビリティ評価

  • 冒頭に要約を置き、読み手が記事全体の目的をつかみやすい構成にしています。
  • 見出しは「概念 → データモデル → API設計 → 運用 → テスト」の順で並べ、必要な章だけ読んでも理解しやすいようにしています。
  • 専門用語は初出で短く補足し、以降は同じ言葉を繰り返して認知負荷を下げています。
  • コード例は短く分割し、1ブロックで1つの責務だけを示しています。
  • 目標レベルはAA相当です。

1. CS向け問い合わせ対応APIは、通常のCRUDと何が違うのか

問い合わせ対応APIは、一見すると「問い合わせを作成して、一覧して、詳細を見て、返信する」だけに見えるかもしれません。けれど実務では、通常のCRUDよりも状態管理と履歴管理が重要になります。

たとえば、問い合わせは一度作られたあと、次のように変化していきます。

  • 顧客から問い合わせが届く
  • CS担当者が内容を確認する
  • 担当者を割り当てる
  • 必要なら社内メモを残す
  • 顧客へ返信する
  • 顧客から追加返信が来る
  • 解決済みにする
  • 一定期間後にクローズする

つまり、問い合わせは「単一レコード」ではなく、会話と状態遷移を持つ業務オブジェクトです。

そのため、問い合わせ対応APIでは次の設計が重要になります。

  • チケット本体
  • メッセージ履歴
  • 内部メモ
  • 担当者
  • ステータス
  • 優先度
  • カテゴリ
  • 監査ログ
  • 通知
  • 検索・絞り込み

FastAPIでは、依存関係システムを使って現在ユーザーや権限を取得し、ルーターを分けて責務を整理できます。Depends はFastAPIの主要機能で、外部コンポーネントや共通処理を簡単に統合できる仕組みとして公式ドキュメントでも説明されています。


2. まず決めたい用語:問い合わせ、チケット、メッセージ、内部メモ

実装に入る前に、用語を整理しておくと設計がぶれにくくなります。

問い合わせ / チケット

顧客から届いた相談や質問のまとまりです。CSツールでは「チケット」と呼ぶことが多いです。この記事では、API上の中心リソースとして Ticket を使います。

メッセージ

顧客または担当者が送信した本文です。1つのチケットに複数のメッセージがぶら下がります。

内部メモ

顧客には見せない社内向けのメモです。たとえば「この顧客はEnterprise契約」「前回も同じ問題があった」「開発チームに確認中」などです。

ステータス

チケットの状態です。例として、次のような値を使います。

  • open
  • pending
  • waiting_customer
  • resolved
  • closed

担当者

問い合わせを担当するCSメンバーです。未担当状態を許可するか、必ず誰かに割り当てるかは運用に合わせます。


3. データモデルの基本形

まずは、Pydanticモデルとして概念を表します。実際にはSQLAlchemyモデルやDBスキーマにも落とし込みますが、最初にAPIの形を考えるためにPydanticで整理します。

from datetime import datetime
from pydantic import BaseModel, EmailStr
from typing import Literal

TicketStatus = Literal[
    "open",
    "pending",
    "waiting_customer",
    "resolved",
    "closed",
]

TicketPriority = Literal["low", "normal", "high", "urgent"]

class TicketRead(BaseModel):
    id: int
    tenant_id: int | None = None
    requester_email: EmailStr
    subject: str
    status: TicketStatus
    priority: TicketPriority
    assignee_id: int | None = None
    created_at: datetime
    updated_at: datetime

メッセージは別モデルにします。

MessageSenderType = Literal["customer", "agent", "system"]

class TicketMessageRead(BaseModel):
    id: int
    ticket_id: int
    sender_type: MessageSenderType
    sender_id: int | None = None
    body: str
    created_at: datetime

内部メモは、顧客向けメッセージと分けます。

class InternalNoteRead(BaseModel):
    id: int
    ticket_id: int
    author_id: int
    body: str
    created_at: datetime

ここで大切なのは、顧客に見えるメッセージと社内だけのメモを混ぜないことです。混ざると、誤って内部メモを顧客へ返してしまうリスクが高まります。


4. ルーター構成:顧客向けとCS向けを分ける

問い合わせ対応APIでは、顧客向けAPIとCS向けAPIを分けるのがおすすめです。

app/
  api/
    v1/
      public/
        tickets.py
      admin/
        tickets.py
        ticket_messages.py
        internal_notes.py

顧客向けは、問い合わせ作成や自分の問い合わせ確認だけに限定します。

POST /tickets
GET /tickets/{ticket_id}
POST /tickets/{ticket_id}/messages

CS向けは、検索、担当者変更、ステータス変更、内部メモ追加などを扱います。

GET /admin/tickets
GET /admin/tickets/{ticket_id}
POST /admin/tickets/{ticket_id}/reply
POST /admin/tickets/{ticket_id}/notes
PATCH /admin/tickets/{ticket_id}/assignee
PATCH /admin/tickets/{ticket_id}/status

このように分けると、認可とレスポンス設計が整理しやすくなります。FastAPIでは APIRouterprefixtags を付けられるため、OpenAPI上でも管理系APIを分けて見せられます。


5. 問い合わせ作成API:顧客が最初に使う入口

まずは、顧客が問い合わせを作成するAPIです。

from fastapi import APIRouter, status
from pydantic import BaseModel, EmailStr, Field

router = APIRouter(prefix="/tickets", tags=["tickets"])

class TicketCreate(BaseModel):
    requester_email: EmailStr
    subject: str = Field(..., min_length=1, max_length=200)
    body: str = Field(..., min_length=1, max_length=5000)

@router.post("", status_code=status.HTTP_201_CREATED)
def create_ticket(payload: TicketCreate):
    return {
        "id": 1,
        "requester_email": payload.requester_email,
        "subject": payload.subject,
        "status": "open",
    }

ここでは status.HTTP_201_CREATED を明示しています。FastAPIの status モジュールにはHTTPステータスコードの定数が用意されており、数字を直接書くより意図が伝わりやすくなります。

問い合わせ作成時には、次のような処理も検討します。

  • スパム対策
  • レート制限
  • 添付ファイル制限
  • 自動受付メール
  • カテゴリ自動分類
  • テナント判定
  • 監査ログまたはイベントログ

最初から全部を入れる必要はありませんが、問い合わせ作成は外部から誰でも叩ける可能性があるため、公開APIとして慎重に扱います。


6. CS向け一覧API:検索と絞り込みを重視する

CS担当者が一番使うのは、チケット一覧です。ここでは、検索条件を明確にします。

from dataclasses import dataclass
from typing import Literal
from fastapi import Depends, Query

@dataclass
class TicketSearchParams:
    q: str | None
    status: str | None
    priority: str | None
    assignee_id: int | None
    limit: int
    offset: int

def get_ticket_search_params(
    q: str | None = Query(default=None, description="件名・本文・メールアドレスで検索"),
    status: Literal["open", "pending", "waiting_customer", "resolved", "closed"] | None = None,
    priority: Literal["low", "normal", "high", "urgent"] | None = None,
    assignee_id: int | None = Query(default=None),
    limit: int = Query(default=50, ge=1, le=200),
    offset: int = Query(default=0, ge=0),
) -> TicketSearchParams:
    return TicketSearchParams(
        q=q,
        status=status,
        priority=priority,
        assignee_id=assignee_id,
        limit=limit,
        offset=offset,
    )

ルーター側はすっきりします。

admin_router = APIRouter(prefix="/admin/tickets", tags=["admin-tickets"])

@admin_router.get("")
def list_tickets(
    params: TicketSearchParams = Depends(get_ticket_search_params),
):
    return {
        "items": [],
        "meta": {
            "limit": params.limit,
            "offset": params.offset,
        },
    }

前回の検索API設計と同じく、検索条件は一覧表示、CSV出力、ダッシュボード集計で再利用できるようにしておくと便利です。


7. 認証と権限:CS担当者でも全員が同じ操作をできるとは限らない

CS向けAPIでは、管理画面ログイン済みであることに加えて、操作ごとの権限が必要です。

例として、次のようなロールを考えます。

  • support
    • チケット閲覧、返信、内部メモ作成
  • support_manager
    • 担当者変更、優先度変更、クローズ
  • admin
    • 全操作、削除、エクスポート

依存関数で管理者を取得します。

from fastapi import Depends, HTTPException, status

class AdminUser:
    def __init__(self, user_id: int, role: str):
        self.user_id = user_id
        self.role = role

def get_current_admin() -> AdminUser:
    return AdminUser(user_id=1, role="support")

def require_support(
    admin: AdminUser = Depends(get_current_admin),
) -> AdminUser:
    if admin.role not in {"support", "support_manager", "admin"}:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="support permission required",
        )
    return admin

担当者変更のような強い操作は別にします。

def require_support_manager(
    admin: AdminUser = Depends(get_current_admin),
) -> AdminUser:
    if admin.role not in {"support_manager", "admin"}:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="manager permission required",
        )
    return admin

FastAPIでは HTTPException をraiseしてクライアントエラーを返せます。公式ドキュメントでも、HTTPException はクライアントエラーや認証・入力エラーなどを示すために使う例外として説明されています。


8. 詳細API:メッセージと内部メモを分けて返す

CS担当者向けの詳細APIでは、チケット本体、顧客とのやり取り、内部メモをまとめて表示したくなります。

@admin_router.get("/{ticket_id}")
def get_ticket_detail(
    ticket_id: int,
    admin: AdminUser = Depends(require_support),
):
    return {
        "ticket": {
            "id": ticket_id,
            "subject": "ログインできません",
            "status": "open",
            "priority": "normal",
        },
        "messages": [
            {
                "id": 1,
                "sender_type": "customer",
                "body": "ログインできません。",
            }
        ],
        "internal_notes": [
            {
                "id": 1,
                "author_id": admin.user_id,
                "body": "同じメールアドレスで過去にも問い合わせあり。",
            }
        ],
    }

ただし、顧客向けAPIでは内部メモを絶対に返さないようにします。

@router.get("/{ticket_id}")
def get_my_ticket(ticket_id: int):
    return {
        "ticket": {
            "id": ticket_id,
            "subject": "ログインできません",
            "status": "open",
        },
        "messages": [
            {
                "id": 1,
                "sender_type": "customer",
                "body": "ログインできません。",
            }
        ],
    }

顧客向けとCS向けでレスポンスモデルを分けることが重要です。同じモデルを使い回すと、うっかり内部情報が混ざる危険があります。


9. 返信API:顧客に見えるメッセージとして保存する

CS担当者が顧客へ返信するAPIです。

from pydantic import BaseModel, Field

class ReplyCreate(BaseModel):
    body: str = Field(..., min_length=1, max_length=5000)

@admin_router.post("/{ticket_id}/reply", status_code=status.HTTP_201_CREATED)
def reply_ticket(
    ticket_id: int,
    payload: ReplyCreate,
    admin: AdminUser = Depends(require_support),
):
    return {
        "ticket_id": ticket_id,
        "message": {
            "sender_type": "agent",
            "sender_id": admin.user_id,
            "body": payload.body,
        },
    }

返信時に考えることは多いです。

  • メッセージをDBへ保存する
  • チケットの updated_at を更新する
  • ステータスを waiting_customer へ変更する
  • 顧客へメール通知する
  • 監査ログを残す

メール通知のような軽い後処理は、FastAPIの BackgroundTasks を使えます。公式ドキュメントでは、レスポンス後に実行したい処理に BackgroundTasks を使えると説明されています。

from fastapi import BackgroundTasks

def send_reply_email(ticket_id: int, body: str) -> None:
    # 実際にはメール送信処理
    pass

@admin_router.post("/{ticket_id}/reply", status_code=status.HTTP_201_CREATED)
def reply_ticket(
    ticket_id: int,
    payload: ReplyCreate,
    background_tasks: BackgroundTasks,
    admin: AdminUser = Depends(require_support),
):
    background_tasks.add_task(send_reply_email, ticket_id, payload.body)
    return {"status": "reply_saved"}

重要なメール送信や再試行が必要な通知は、ジョブキューへ逃がす方が安全です。BackgroundTasks は軽い処理向けと考えるとよいです。


10. 内部メモAPI:顧客に見せない会話を安全に扱う

内部メモはCS運用で非常に便利です。けれど、顧客に見せない情報を扱うため、APIとして明確に分ける必要があります。

class InternalNoteCreate(BaseModel):
    body: str = Field(..., min_length=1, max_length=3000)

@admin_router.post("/{ticket_id}/notes", status_code=status.HTTP_201_CREATED)
def create_internal_note(
    ticket_id: int,
    payload: InternalNoteCreate,
    admin: AdminUser = Depends(require_support),
):
    return {
        "ticket_id": ticket_id,
        "note": {
            "author_id": admin.user_id,
            "body": payload.body,
        },
    }

内部メモには、顧客に見せられない情報が含まれる可能性があります。たとえば、契約状況、調査メモ、別部署への依頼などです。

そのため、内部メモは次のように扱います。

  • 顧客向けAPIには絶対に含めない
  • 検索対象にするかどうかを慎重に決める
  • 監査ログに作成者と作成時刻を残す
  • 削除や編集の権限を厳しくする

11. ステータス遷移:許可された流れだけ通す

問い合わせ対応では、ステータス遷移を明確にすることが重要です。

例として、次のような遷移を考えます。

open -> pending
open -> waiting_customer
pending -> waiting_customer
waiting_customer -> open
waiting_customer -> resolved
resolved -> closed
closed -> open

このルールをサービス層に置きます。

ALLOWED_TRANSITIONS = {
    "open": {"pending", "waiting_customer", "resolved"},
    "pending": {"waiting_customer", "resolved"},
    "waiting_customer": {"open", "resolved"},
    "resolved": {"closed", "open"},
    "closed": {"open"},
}

def can_transition(current: str, next_status: str) -> bool:
    return next_status in ALLOWED_TRANSITIONS.get(current, set())

APIではこの関数を使います。

class TicketStatusUpdate(BaseModel):
    status: TicketStatus

@admin_router.patch("/{ticket_id}/status")
def update_ticket_status(
    ticket_id: int,
    payload: TicketStatusUpdate,
    admin: AdminUser = Depends(require_support),
):
    current_status = "open"

    if not can_transition(current_status, payload.status):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="invalid status transition",
        )

    return {
        "ticket_id": ticket_id,
        "status": payload.status,
    }

状態遷移に失敗した場合は、409 Conflict が自然です。FastAPIの status モジュールを使うと、こうしたステータスコードも読みやすく書けます。


12. 担当者アサインAPI:誰が責任を持つかを明確にする

問い合わせが増えると、担当者管理が必要になります。

class AssigneeUpdate(BaseModel):
    assignee_id: int | None

@admin_router.patch("/{ticket_id}/assignee")
def update_assignee(
    ticket_id: int,
    payload: AssigneeUpdate,
    admin: AdminUser = Depends(require_support_manager),
):
    return {
        "ticket_id": ticket_id,
        "assignee_id": payload.assignee_id,
        "updated_by": admin.user_id,
    }

担当者変更では、次のようなルールを考えます。

  • support_manager 以上だけが他人へ割り当て可能
  • support は自分への引き取りだけ可能
  • クローズ済みチケットは担当者変更不可
  • 担当者変更時に通知する
  • 監査ログを残す

単純な PATCH に見えますが、実務上はかなり重要な操作です。


13. 優先度とカテゴリ:検索しやすくするためのメタ情報

問い合わせには、優先度とカテゴリを持たせると運用しやすくなります。

カテゴリ例:

  • login
  • billing
  • bug
  • feature_request
  • account
  • other

優先度例:

  • low
  • normal
  • high
  • urgent
class TicketMetaUpdate(BaseModel):
    priority: TicketPriority | None = None
    category: str | None = None

@admin_router.patch("/{ticket_id}/meta")
def update_ticket_meta(
    ticket_id: int,
    payload: TicketMetaUpdate,
    admin: AdminUser = Depends(require_support),
):
    return {
        "ticket_id": ticket_id,
        "priority": payload.priority,
        "category": payload.category,
    }

カテゴリと優先度を整えることで、ダッシュボードやレポートも作りやすくなります。たとえば、「請求カテゴリの未解決件数」「urgentの平均初回返信時間」などを可視化できます。


14. 監査ログ:CS対応はあとから説明できることが大切

問い合わせ対応では、監査ログがとても重要です。

残したいイベント例:

  • ticket.create
  • ticket.reply
  • ticket.note.create
  • ticket.status.update
  • ticket.assignee.update
  • ticket.priority.update
  • ticket.close
  • ticket.reopen

監査ログ関数を用意します。

def write_audit_log(
    actor_id: int | None,
    action: str,
    resource_type: str,
    resource_id: str,
    detail: dict | None = None,
) -> None:
    # 実際はDBや構造化ログへ保存
    pass

返信時に使います。

@admin_router.post("/{ticket_id}/reply", status_code=status.HTTP_201_CREATED)
def reply_ticket(
    ticket_id: int,
    payload: ReplyCreate,
    admin: AdminUser = Depends(require_support),
):
    write_audit_log(
        actor_id=admin.user_id,
        action="ticket.reply",
        resource_type="ticket",
        resource_id=str(ticket_id),
        detail={"body_length": len(payload.body)},
    )
    return {"status": "reply_saved"}

本文そのものを監査ログに残すかどうかは慎重に決めます。個人情報や機密情報が含まれる可能性があるため、長さやメッセージIDだけ残す設計も有効です。


15. 通知設計:顧客通知と社内通知を分ける

問い合わせ対応では、通知が複数種類あります。

顧客向け通知

  • 問い合わせ受付メール
  • CS返信メール
  • 解決確認メール

社内向け通知

  • 新規問い合わせ通知
  • urgentチケット通知
  • 自分にアサインされた通知
  • SLA期限が近い通知

これらは同じ「通知」でも、宛先と目的が違います。API設計上も区別しておくとよいです。

def notify_customer_reply(ticket_id: int) -> None:
    pass

def notify_assignee_changed(ticket_id: int, assignee_id: int) -> None:
    pass

軽い通知は BackgroundTasks、再試行が必要な通知はジョブキューへ送るのがおすすめです。


16. SLAを見据えた設計:対応期限を持たせる

CS運用が本格化すると、SLAや対応期限が必要になります。

例:

  • 初回返信は24時間以内
  • urgentは2時間以内
  • Enterprise顧客は優先対応
  • resolved後7日で自動クローズ

チケットに期限を持たせます。

class TicketSLAInfo(BaseModel):
    first_response_due_at: datetime | None = None
    resolution_due_at: datetime | None = None
    breached: bool = False

SLAを持たせると、検索APIも強くなります。

GET /admin/tickets?sla=breached
GET /admin/tickets?priority=urgent&status=open

将来的には、期限切れチケットを定期ジョブで検出し、社内通知する設計へ進められます。


17. 顧客情報との連携:チケットだけで完結させない

問い合わせ対応では、チケット本文だけでは十分ではありません。CS担当者は、次の情報も見たいことが多いです。

  • 顧客の契約プラン
  • テナント名
  • 支払い状態
  • 利用中の機能
  • 過去の問い合わせ
  • 最近のエラー
  • 管理者操作履歴

ただし、詳細APIに全部詰め込むと重くなります。最初は、チケット詳細に必要最低限の顧客サマリーを含める程度がよいです。

@admin_router.get("/{ticket_id}")
def get_ticket_detail(
    ticket_id: int,
    admin: AdminUser = Depends(require_support),
):
    return {
        "ticket": {"id": ticket_id, "subject": "請求について"},
        "customer_summary": {
            "tenant_name": "Example Inc.",
            "plan": "pro",
            "subscription_status": "active",
        },
        "messages": [],
        "internal_notes": [],
    }

より詳しい情報は、別APIへ分けると保守しやすくなります。


18. エラー設計:CS担当者が判断できるエラーにする

CS向けAPIでは、エラーが曖昧だと現場が困ります。

たとえば、次のような区別が必要です。

  • 権限不足
  • チケットが存在しない
  • すでにクローズ済み
  • 無効なステータス遷移
  • 担当者にできないユーザー
  • 通知送信に失敗したが返信保存は成功

FastAPIでは HTTPException を使って、ステータスコードと詳細を明示できます。

raise HTTPException(
    status_code=status.HTTP_409_CONFLICT,
    detail={
        "code": "INVALID_TICKET_STATUS_TRANSITION",
        "message": "このステータスには変更できません。",
    },
)

エラー形式は、これまでの記事で扱った共通エラーフォーマットに揃えるとよいです。CS向け画面では、code を見てUI上の表示や操作案内を分けられます。


19. テスト方針:会話履歴と状態遷移を守る

問い合わせ対応APIでは、次のようなテストが重要です。

  • 顧客が問い合わせを作成できる
  • CS担当者が一覧検索できる
  • 権限のないユーザーは詳細を見られない
  • 返信するとメッセージが増える
  • 返信するとステータスが waiting_customer になる
  • 内部メモは顧客向けAPIに出ない
  • 無効なステータス遷移は409になる
  • 担当者変更にはmanager権限が必要
  • 監査ログが残る
  • 通知タスクが登録される

FastAPIにはテスト時に依存関係を上書きする仕組みがあります。公式ドキュメントでは、app.dependency_overrides を使って依存関数を差し替えられると説明されています。これにより、テスト用の管理者やCS担当者を簡単に差し込めます。


20. よくある失敗パターン

20.1 メッセージと内部メモを同じテーブル・同じレスポンスで雑に扱う

内部メモが顧客へ漏れるリスクがあります。顧客向けメッセージと内部メモは、APIレスポンス上でも明確に分けるのがおすすめです。

20.2 ステータスを自由に変更できるようにする

open からいきなり closedclosed から何度も pending など、業務上おかしな状態になりやすくなります。許可された遷移だけ通すサービス層を作ると安全です。

20.3 返信保存とメール送信を完全に一体化する

メール送信に失敗したせいで返信保存まで失敗すると、CS担当者の操作体験が悪くなります。返信保存を主処理、メール送信を後続処理として分けると安定します。

20.4 権限を「管理者なら全部OK」にする

CS、経理、開発、superadminで見られる情報や操作できる範囲は違います。問い合わせは個人情報を含みやすいため、権限は細かく分ける価値があります。

20.5 監査ログを残さない

「誰がこの返信をしたのか」「誰がクローズしたのか」が分からないと、問い合わせ対応の品質管理が難しくなります。最低限、返信、内部メモ、ステータス変更、担当者変更は残しましょう。


21. 読者別ロードマップ

個人開発・学習者さん

  1. まずは POST /tickets で問い合わせ作成を作る
  2. メッセージ履歴を別モデルにする
  3. CS向け /admin/tickets を分ける
  4. 返信APIと内部メモAPIを分ける
  5. ステータス遷移を小さな関数で制御する

小規模チームのエンジニアさん

  1. CSの実際の業務フローを聞き取る
  2. チケット、メッセージ、内部メモ、担当者、ステータスを分けて設計する
  3. support / support_manager / admin の権限を定義する
  4. 返信、メモ、ステータス変更、担当者変更に監査ログを入れる
  5. 一覧検索とCSV出力を共通条件にする

SaaS開発チーム・スタートアップの皆さま

  1. 問い合わせ対応APIをCS業務基盤として扱う
  2. テナント、契約、監査ログ、管理画面APIと連携する
  3. SLA、優先度、エスカレーションを設計する
  4. 通知基盤やジョブキューと接続する
  5. 対応時間、未解決件数、カテゴリ別件数をメトリクス化する

参考リンク


まとめ

  • CS向け問い合わせ対応APIは、単なる問い合わせ保存機能ではなく、会話履歴、内部メモ、担当者、ステータス、監査ログ、通知を含む業務基盤です。
  • FastAPIでは、APIRouter で顧客向けAPIとCS向けAPIを分け、Depends で権限を整理し、HTTPExceptionstatus で分かりやすいエラーを返す設計が扱いやすいです。
  • 顧客に見えるメッセージと、社内だけの内部メモは必ず分けることが大切です。ここを混ぜると情報漏えいにつながりやすくなります。
  • ステータス遷移は自由入力ではなく、許可された流れだけをサービス層で制御すると安全です。
  • 最初から完璧なCS基盤を作る必要はありません。まずはチケット、メッセージ、内部メモ、ステータス、監査ログの5つを分けるだけでも、問い合わせ対応APIはかなり実務に耐えやすくなります。

次の記事としては、この流れと相性が良い「FastAPIで実践する通知・メール送信基盤設計」や、「FastAPIで作るSLA・エスカレーション管理API設計」が自然につながります。

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)