サイトアイコン IT & ライフハックブログ|学びと実践のためのアイデア集

FastAPIで作るSLA・エスカレーション管理API設計入門:問い合わせ対応を遅らせないための実務パターン

green snake

Photo by Pixabay on Pexels.com

FastAPIで作るSLA・エスカレーション管理API設計入門:問い合わせ対応を遅らせないための実務パターン


要約

  • SLA管理とは、問い合わせやチケットに対して「いつまでに初回返信するか」「いつまでに解決するか」を明確にし、期限超過を防ぐための仕組みです。
  • FastAPIでは、チケット作成時にSLA期限を計算し、期限が近いものや超過したものを検索・通知・エスカレーションできるAPIとして設計すると、CS運用が安定します。
  • 重要なのは、SLAを単なる日時カラムとして扱わないことです。優先度、プラン、営業時間、担当者、ステータス、休業日などを考慮した「業務ルール」としてサービス層に閉じ込めるのが安全です。
  • エスカレーションは、期限超過後に上位担当者や別チームへ引き上げる仕組みです。通知、担当者変更、優先度変更、監査ログを組み合わせると実務に耐えやすくなります。
  • 本記事では、FastAPIでSLA・エスカレーション管理APIを作るためのデータモデル、期限計算、検索API、通知、監査ログ、テスト戦略までを段階的に整理します。

誰が読んで得をするか

個人開発・学習者さん

問い合わせ対応機能を作ったあと、「未対応の問い合わせが埋もれてしまう」「どれを先に対応すべきか分からない」と感じ始めた方に向いています。
最初はチケットに status だけあれば十分に見えますが、問い合わせが増えると、対応期限や優先度の管理が必要になります。この記事では、SLAの考え方を小さく導入する方法から説明します。

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

CS担当者が複数人になり、問い合わせの担当者、優先度、期限超過、上長への通知が必要になってきたチームに向いています。
「urgentだけ早く返す」「Enterprise顧客は優先する」「未返信が24時間を超えたらマネージャーへ通知する」といったルールを、FastAPIのサービス層やジョブ処理としてどう表現するかを整理できます。

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

SLAが顧客契約やCS品質に直結しているチームに向いています。
問い合わせ対応時間、初回返信時間、解決時間、エスカレーション率、期限超過率は、顧客満足度や解約率にも影響します。FastAPIでSLA管理をAPIとして整えることで、CSダッシュボード、通知基盤、監査ログ、レポート出力とも連携しやすくなります。


アクセシビリティ評価

  • 冒頭に要約と対象読者を置き、記事の目的を先に把握できる構成にしています。
  • 「SLA」「エスカレーション」「初回返信期限」などの用語は、初出時に短く補足しています。
  • コード例は短く分割し、1つのブロックで1つの責務だけを扱っています。
  • 見出しだけ追っても、概念から実装、運用、テストまでの流れが分かるようにしています。
  • 目標レベルはAA相当です。

1. SLAとは何か:問い合わせ対応の「約束」をコードで守る

SLAは、Service Level Agreement の略で、サービス提供者と利用者の間で合意するサービス水準を意味します。問い合わせ対応の文脈では、主に次のような約束として扱われます。

  • 初回返信は24時間以内
  • urgentの問い合わせは2時間以内
  • Enterprise顧客は優先対応
  • 解決目標は3営業日以内
  • 営業時間外は期限計算から除外する

SLA管理を入れる目的は、単に期限を表示することではありません。
本当に重要なのは、対応漏れを防ぎ、期限超過を早く検知し、必要なら担当者や上位チームへ引き上げることです。

そのため、SLA管理には次の要素が必要です。

  • 期限計算
  • 期限超過判定
  • 優先度管理
  • 担当者管理
  • 通知
  • エスカレーション
  • 監査ログ
  • レポート

FastAPIでは、これらをルーターに直接書くのではなく、サービス層やポリシー関数として分けると管理しやすくなります。


2. エスカレーションとは何か:対応を上位へ引き上げる仕組み

エスカレーションとは、問い合わせが一定条件を満たしたときに、上位担当者や別チームへ対応を引き上げることです。

たとえば、次のようなケースがあります。

  • 初回返信期限を超えた
  • 解決期限を超えた
  • urgentなのに未担当のまま
  • 担当者が一定時間更新していない
  • 顧客がEnterpriseプラン
  • 同じ顧客から短時間に複数の問い合わせが来た

エスカレーションは、単なる通知ではありません。
実務では、次のようなアクションを伴うことがあります。

  • マネージャーへ通知する
  • 担当者を自動変更する
  • 優先度を引き上げる
  • 内部メモを追加する
  • SLA違反として記録する
  • 監査ログを残す

つまり、エスカレーションは「対応漏れを見える化し、次の行動へつなげる業務処理」です。


3. 基本データモデル:チケットにSLA情報を持たせる

まずは、問い合わせチケットにSLA関連の項目を持たせます。

from datetime import datetime
from pydantic import BaseModel
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: str
    subject: str
    status: TicketStatus
    priority: TicketPriority
    assignee_id: int | None = None
    first_response_due_at: datetime | None = None
    resolution_due_at: datetime | None = None
    first_responded_at: datetime | None = None
    resolved_at: datetime | None = None
    escalated: bool = False
    created_at: datetime
    updated_at: datetime

ここで重要な項目は次の通りです。

  • first_response_due_at
    初回返信の期限
  • resolution_due_at
    解決期限
  • first_responded_at
    初回返信が実際に行われた時刻
  • resolved_at
    解決時刻
  • escalated
    すでにエスカレーション済みかどうか

このように、期限と実績を両方持つことで、SLA達成率や対応時間を後から計算できます。


4. SLAルールをプラン・優先度ごとに定義する

SLAは、すべての問い合わせで同じとは限りません。
優先度や契約プランによって変わることが多いです。

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

from dataclasses import dataclass
from datetime import timedelta

@dataclass(frozen=True)
class SLARule:
    first_response_within: timedelta
    resolution_within: timedelta

SLA_RULES = {
    ("free", "normal"): SLARule(
        first_response_within=timedelta(hours=48),
        resolution_within=timedelta(days=7),
    ),
    ("pro", "normal"): SLARule(
        first_response_within=timedelta(hours=24),
        resolution_within=timedelta(days=3),
    ),
    ("enterprise", "normal"): SLARule(
        first_response_within=timedelta(hours=8),
        resolution_within=timedelta(days=2),
    ),
    ("enterprise", "urgent"): SLARule(
        first_response_within=timedelta(hours=2),
        resolution_within=timedelta(hours=12),
    ),
}

ここでは、plan_codepriority の組み合わせでSLAを決めています。
実務では、さらに契約単位の例外や特別SLAが入ることもあります。

その場合でも、ルールを一箇所に寄せておくと変更しやすくなります。


5. SLA期限を計算するサービスを作る

チケット作成時に、SLA期限を計算します。

from datetime import datetime, timezone

def resolve_sla_rule(plan_code: str, priority: str) -> SLARule:
    return SLA_RULES.get(
        (plan_code, priority),
        SLA_RULES[("free", "normal")],
    )

def calculate_sla_deadlines(
    created_at: datetime,
    plan_code: str,
    priority: str,
) -> tuple[datetime, datetime]:
    rule = resolve_sla_rule(plan_code, priority)

    first_response_due_at = created_at + rule.first_response_within
    resolution_due_at = created_at + rule.resolution_within

    return first_response_due_at, resolution_due_at

この例では単純に現在時刻へ時間を足しています。
ただし、実務では営業時間や休業日を考慮する必要が出てきます。

最初は単純な計算でもよいですが、将来的に次の要素が入り得ることを想定しておくとよいです。

  • 営業時間
  • 土日祝日
  • テナントごとのタイムゾーン
  • プランごとの特別SLA
  • 年末年始やメンテナンス日

このため、期限計算はルーターに直書きせず、必ずサービス層に切り出すのがおすすめです。


6. 問い合わせ作成時にSLA期限を設定する

チケット作成サービスの中で、SLA期限を計算します。

from datetime import datetime, timezone
from pydantic import BaseModel

class TicketCreate(BaseModel):
    requester_email: str
    subject: str
    body: str
    priority: TicketPriority = "normal"

class TicketService:
    def create_ticket(
        self,
        payload: TicketCreate,
        tenant_plan: str,
    ) -> dict:
        now = datetime.now(timezone.utc)

        first_due, resolution_due = calculate_sla_deadlines(
            created_at=now,
            plan_code=tenant_plan,
            priority=payload.priority,
        )

        ticket = {
            "id": 1,
            "requester_email": payload.requester_email,
            "subject": payload.subject,
            "status": "open",
            "priority": payload.priority,
            "first_response_due_at": first_due,
            "resolution_due_at": resolution_due,
            "created_at": now,
            "updated_at": now,
        }

        return ticket

これにより、チケット作成時点で「いつまでに対応すべきか」が決まります。
後から毎回計算するより、チケット作成時に期限を保存しておく方が、検索や通知がしやすくなります。


7. 初回返信を記録する

SLA達成率を測るには、初回返信時刻が必要です。
CS担当者が最初に顧客へ返信したタイミングで、first_responded_at を設定します。

from datetime import datetime, timezone

class TicketReplyService:
    def reply_to_ticket(
        self,
        ticket: dict,
        agent_id: int,
        body: str,
    ) -> dict:
        now = datetime.now(timezone.utc)

        # 初回返信がまだなら記録
        if ticket.get("first_responded_at") is None:
            ticket["first_responded_at"] = now

        ticket["status"] = "waiting_customer"
        ticket["updated_at"] = now

        message = {
            "ticket_id": ticket["id"],
            "sender_type": "agent",
            "sender_id": agent_id,
            "body": body,
            "created_at": now,
        }

        return message

この処理も、ルーターではなくサービス層に置くのが安全です。
返信APIが複数あっても、初回返信のルールを一箇所で守れるからです。


8. SLA違反を判定する関数を作る

期限超過の判定を関数化します。

from datetime import datetime

def is_first_response_breached(ticket: dict, now: datetime) -> bool:
    if ticket.get("first_responded_at") is not None:
        return False
    due_at = ticket.get("first_response_due_at")
    return due_at is not None and now > due_at

def is_resolution_breached(ticket: dict, now: datetime) -> bool:
    if ticket.get("resolved_at") is not None:
        return False
    if ticket.get("status") in {"resolved", "closed"}:
        return False
    due_at = ticket.get("resolution_due_at")
    return due_at is not None and now > due_at

このように、初回返信SLAと解決SLAは別々に見る方がよいです。
問い合わせ対応では、「初回返信は早かったが解決が遅い」「初回返信すら遅れている」という状態が異なる意味を持つからです。


9. SLA検索API:期限超過や期限間近のチケットを探す

CS管理画面では、SLA違反や期限間近のチケットをすぐ見つけられる必要があります。

from fastapi import APIRouter, Query
from typing import Literal

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

@router.get("/sla")
def list_sla_tickets(
    kind: Literal["breached", "due_soon"] = Query(default="breached"),
    target: Literal["first_response", "resolution"] = Query(default="first_response"),
    limit: int = Query(default=50, ge=1, le=200),
):
    return {
        "items": [],
        "meta": {
            "kind": kind,
            "target": target,
            "limit": limit,
        },
    }

このような専用APIがあると、CSダッシュボードやマネージャー画面で使いやすくなります。

たとえば、次のような画面を作れます。

  • 初回返信期限を超えたチケット
  • 解決期限を超えたチケット
  • 2時間以内に期限切れになるurgentチケット
  • Enterprise顧客の未対応チケット
  • 担当者未設定で期限が近いチケット

SLA検索は、通常の問い合わせ検索とは別にしてもよいくらい重要です。


10. エスカレーションルールを定義する

次に、期限超過したチケットをどう扱うかを定義します。

from dataclasses import dataclass
from datetime import timedelta

@dataclass(frozen=True)
class EscalationRule:
    after_due: timedelta
    notify_role: str
    raise_priority: bool = False
    assign_to_manager: bool = False

ESCALATION_RULES = {
    "first_response": EscalationRule(
        after_due=timedelta(minutes=0),
        notify_role="support_manager",
        raise_priority=True,
    ),
    "resolution": EscalationRule(
        after_due=timedelta(hours=1),
        notify_role="support_manager",
        raise_priority=True,
        assign_to_manager=True,
    ),
}

この例では、初回返信期限を超えたらすぐに通知し、解決期限を超えて1時間経ったらマネージャーへ割り当てる、という考え方です。

実務では、プランや優先度によってエスカレーションルールを変えることもあります。


11. エスカレーション処理のサービスを作る

エスカレーション処理は、チケットを検索して、必要に応じて状態を更新し、通知と監査ログを残します。

from datetime import datetime, timezone

class EscalationService:
    def escalate_ticket(
        self,
        ticket: dict,
        reason: str,
    ) -> dict:
        now = datetime.now(timezone.utc)

        ticket["escalated"] = True
        ticket["updated_at"] = now

        if ticket.get("priority") != "urgent":
            ticket["priority"] = "high"

        escalation_event = {
            "ticket_id": ticket["id"],
            "reason": reason,
            "created_at": now,
        }

        return escalation_event

この例では簡略化していますが、実務では次のような処理が入ります。

  • エスカレーション履歴を保存する
  • 監査ログを残す
  • マネージャーへ通知する
  • 担当者を変更する
  • ダッシュボード上で目立たせる

12. 定期ジョブでSLA違反を検出する

SLA違反は、ユーザーがAPIを叩いたタイミングだけで検出するのでは不十分です。
定期的にチェックするジョブが必要になります。

たとえば、5分ごとに次を行います。

  1. 未返信で初回返信期限を超えたチケットを探す
  2. 未解決で解決期限を超えたチケットを探す
  3. まだエスカレーションしていないものを処理する
  4. 通知と監査ログを残す

FastAPI単体でも軽い処理なら BackgroundTasks を使えますが、定期実行や再試行が必要なものはCeleryやスケジューラの導入が自然です。

def run_sla_check_job():
    now = datetime.now(timezone.utc)

    # 実際はDBから対象チケットを検索
    overdue_tickets = []

    for ticket in overdue_tickets:
        if is_first_response_breached(ticket, now):
            EscalationService().escalate_ticket(
                ticket,
                reason="first_response_sla_breached",
            )

SLAチェックは運用上かなり重要なので、ジョブの成功・失敗・対象件数もログやメトリクスに残すことをおすすめします。


13. 通知設計:誰に、いつ、何を知らせるか

エスカレーションは通知とセットで考えます。
通知先は、運用によってさまざまです。

  • 担当者本人
  • support_manager
  • CSチームのSlackチャンネル
  • Enterprise専用担当者
  • 開発チーム
  • 経理や請求担当

通知内容には、次の情報があると実務で使いやすいです。

  • チケットID
  • 件名
  • 顧客名またはテナント名
  • 優先度
  • 期限
  • 超過時間
  • 担当者
  • 管理画面URL

ただし、メールやSlackには個人情報や機密情報を入れすぎないように注意します。
詳細は管理画面へリンクし、通知本文は最小限にすると安全です。


14. エスカレーション履歴を残す

エスカレーションは、あとから追跡できるように履歴として残すべきです。

from datetime import datetime
from pydantic import BaseModel

class EscalationEventRead(BaseModel):
    id: int
    ticket_id: int
    reason: str
    from_assignee_id: int | None = None
    to_assignee_id: int | None = None
    created_at: datetime

理由の例:

  • first_response_sla_breached
  • resolution_sla_breached
  • urgent_unassigned
  • manual_escalation
  • enterprise_customer

エスカレーション履歴があると、「なぜこのチケットが上がってきたのか」をCSマネージャーが理解しやすくなります。


15. 手動エスカレーションAPIも用意する

自動エスカレーションだけでなく、CS担当者が手動で引き上げたい場合もあります。

from pydantic import BaseModel, Field
from fastapi import Depends, status

class ManualEscalationRequest(BaseModel):
    reason: str = Field(..., min_length=1, max_length=1000)

@router.post("/{ticket_id}/escalate", status_code=status.HTTP_200_OK)
def escalate_ticket_manually(
    ticket_id: int,
    payload: ManualEscalationRequest,
    admin=Depends(require_support),
):
    return {
        "ticket_id": ticket_id,
        "escalated": True,
        "reason": payload.reason,
        "performed_by": admin.user_id,
    }

手動エスカレーションでは、必ず理由を入力させるのがおすすめです。
あとから履歴を見たときに、なぜ引き上げられたのか分からないと運用が難しくなるからです。


16. SLAダッシュボードAPIを作る

SLA運用では、一覧だけでなく集計も重要です。

たとえば、次のような指標を返すAPIがあると便利です。

  • 未対応チケット数
  • 初回返信SLA違反数
  • 解決SLA違反数
  • urgent未対応数
  • 担当者未設定数
  • 平均初回返信時間
  • 平均解決時間
@router.get("/sla/summary")
def get_sla_summary():
    return {
        "open_count": 42,
        "first_response_breached_count": 3,
        "resolution_breached_count": 5,
        "urgent_open_count": 2,
        "unassigned_count": 7,
        "avg_first_response_minutes": 85,
        "avg_resolution_hours": 36,
    }

このAPIがあると、CSマネージャーが日次・週次で状況を確認しやすくなります。
将来的には、GrafanaやBIツールへメトリクスとして渡すこともできます。


17. 営業時間を考慮する場合の注意点

SLAで最も難しいのが、営業時間や休業日を含む計算です。

例:

  • 平日10:00〜18:00だけカウント
  • 土日祝日は除外
  • テナントごとにタイムゾーンが違う
  • Enterpriseだけ24時間対応
  • 年末年始は特別ルール

この場合、単純な created_at + timedelta(hours=24) では不正確になります。

最初は単純なUTC基準で始めても構いませんが、将来的に営業時間を入れるなら、次のように抽象化しておくとよいです。

class BusinessCalendar:
    def add_business_time(
        self,
        start_at: datetime,
        duration_minutes: int,
        timezone_name: str,
    ) -> datetime:
        ...

このように、期限計算を BusinessCalendar へ閉じ込めておけば、後から祝日や営業時間を追加しやすくなります。


18. 監査ログ:SLA変更とエスカレーションは必ず残す

SLAやエスカレーションは、CS品質や顧客説明に関わります。
そのため、次の操作は監査ログ対象にするのがおすすめです。

  • SLA期限の手動変更
  • 優先度変更
  • 手動エスカレーション
  • 自動エスカレーション
  • 担当者変更
  • チケット解決
  • チケット再オープン

監査ログ例:

def write_audit_log(
    actor_id: int | None,
    action: str,
    resource_type: str,
    resource_id: str,
    detail: dict | None = None,
) -> None:
    ...
write_audit_log(
    actor_id=admin.user_id,
    action="ticket.escalate",
    resource_type="ticket",
    resource_id=str(ticket_id),
    detail={"reason": payload.reason},
)

自動エスカレーションの場合は、actor_id=Noneactor_type="system" として残すと分かりやすいです。


19. SLA違反時のユーザー体験も考える

SLAは社内向け指標に見えますが、顧客体験にも影響します。
期限を超えたときに、顧客へ何を見せるかも考えておくとよいです。

例:

  • 「現在確認中です」と表示する
  • 遅延のお詫びメッセージを自動送信する
  • Enterprise顧客だけ専用担当者へ通知する
  • 解決が長引いている場合、進捗連絡を促す

ただし、自動メッセージを送りすぎると逆効果になることもあります。
SLA違反時の顧客通知は、CSチームと相談して慎重に決めるのがおすすめです。


20. テスト方針:期限計算と遷移を重点的に守る

SLA管理は、時刻に依存するためテストがとても重要です。

最低限、次のテストを用意すると安心です。

  • freeプランのSLA期限が正しく計算される
  • enterprise urgentの期限が短く計算される
  • 初回返信済みなら初回返信SLA違反にならない
  • 未返信で期限超過なら違反になる
  • resolvedなら解決SLA違反にならない
  • エスカレーション済みチケットを二重エスカレーションしない
  • 手動エスカレーションには理由が必須
  • 監査ログが残る
  • SLAサマリーAPIの件数が正しい

20.1 期限判定のテスト例

from datetime import datetime, timedelta, timezone

def test_first_response_breached_when_due_passed():
    now = datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)
    ticket = {
        "first_responded_at": None,
        "first_response_due_at": now - timedelta(minutes=1),
    }

    assert is_first_response_breached(ticket, now) is True

20.2 違反しないケースも必ず見る

def test_first_response_not_breached_when_already_responded():
    now = datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)
    ticket = {
        "first_responded_at": now - timedelta(hours=1),
        "first_response_due_at": now - timedelta(minutes=1),
    }

    assert is_first_response_breached(ticket, now) is False

SLAは「違反になる条件」と同じくらい、「違反にならない条件」をテストすることが大切です。


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

21.1 SLA期限を毎回その場で再計算する

ルール変更後に過去チケットの期限まで変わってしまう可能性があります。
作成時点の期限は保存しておく方が安全です。

21.2 初回返信と解決期限を混ぜる

初回返信は早くても、解決が遅いケースがあります。
別々の期限と実績時刻を持つことをおすすめします。

21.3 エスカレーションを通知だけで済ませる

通知だけでは履歴が残りづらく、あとから追跡できません。
エスカレーションイベントとして保存しましょう。

21.4 二重エスカレーションする

定期ジョブで同じチケットを何度もエスカレーションしてしまうことがあります。
escalated フラグやエスカレーション履歴で冪等性を守ります。

21.5 営業時間を後から直書きで足す

営業時間計算は複雑化しやすいです。
最初から BusinessCalendar のような抽象へ寄せると後が楽です。


22. 読者別ロードマップ

個人開発・学習者さん

  1. チケットに first_response_due_at を追加する
  2. 優先度ごとのSLAルールを小さく作る
  3. 初回返信時刻を記録する
  4. 期限超過判定関数を作る
  5. SLA違反一覧APIを作る

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

  1. CSチームとSLAルールを棚卸しする
  2. 初回返信SLAと解決SLAを分ける
  3. 定期ジョブで違反チケットを検出する
  4. 自動エスカレーションと手動エスカレーションを実装する
  5. 監査ログと通知を入れる

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

  1. プランごとのSLAを定義する
  2. 営業時間・タイムゾーン・休業日の扱いを整理する
  3. SLAサマリーAPIとダッシュボードを作る
  4. エスカレーション履歴を保存し、通知基盤と連携する
  5. SLA達成率、平均初回返信時間、解決時間をメトリクス化する

参考リンク


まとめ

  • SLA管理は、問い合わせ対応における「いつまでに対応すべきか」をコードで守るための仕組みです。
  • FastAPIでは、SLA期限計算、違反判定、エスカレーション処理をサービス層へ切り出し、ルーターは薄く保つと扱いやすくなります。
  • 初回返信期限と解決期限は別々に持ち、実績時刻も保存すると、SLA達成率や平均対応時間を後から計算できます。
  • エスカレーションは通知だけでなく、履歴・監査ログ・担当者変更・優先度変更まで含めて設計すると実務に強くなります。
  • まずは単純なUTC基準の期限計算から始めても大丈夫です。将来、営業時間や休業日を入れられるよう、期限計算をサービス層へ閉じ込めておくのがポイントです。

次の記事としては、この流れと相性が良い「FastAPIで実践するイベント駆動アーキテクチャ入門」や、「FastAPIで作るCSダッシュボードAPI設計」が自然につながります。

モバイルバージョンを終了