# API 레퍼런스

기본 URL: `https://trendy.storydot.kr/superapp/api` · 인증: `X-Client-Id` + `X-API-Key` ·
응답 봉투: `{ "success", "data", "message" }` (실패 시 `errorCode`).
주체 식별자는 항상 파트너 전용 익명 ID(`pairwiseSub`) — civilId/userId는 절대 노출되지 않습니다.

## POST /fi/score/query — 단건 점수 조회

요청:

```json
{
  "pairwiseSub": "b41c9d2f-8e77-4a10-9c3e-example",
  "purpose": "UNDERWRITING"
}
```

응답 200:

```json
{
  "success": true,
  "data": {
    "pairwiseSub": "b41c9d2f-8e77-4a10-9c3e-example",
    "score": 712,
    "scoreMin": 0,
    "scoreMax": 850,
    "tier": "GOOD",
    "pullType": "SOFT",
    "reasonCodes": [
      {
        "code": "RC_LOW_UTILITY_PAYMENT_RELIABILITY",
        "labelEn": "Low utility bill payment reliability",
        "labelLocalized": "전기·수도 등 공과금을 제때 낸 기록이 부족함",
        "category": "PAYMENT",
        "impactPoints": 38,
        "rank": 1
      }
    ],
    "modelVersion": "scorecard-v2.1",
    "scoredAt": "2026-08-20 14:32:11",
    "decisionHash": "9f2ab1c4e5d6...64hex",
    "environment": "LIVE",
    "confidence": "HIGH",
    "dataSufficiency": { "featuresObserved": 18, "featuresTotal": 23, "coveragePercent": 78.3 }
  },
  "message": "Score retrieved"
}
```

- `tier`: EXCELLENT(750–850) / GOOD(650–749) / FAIR(550–649) / BELOW_AVERAGE(450–549) / POOR(0–449)
- `reasonCodes`: 카탈로그 31종 중 최대 4건, 영향 순
- `decisionHash`: 프로비넌스 해시(있을 때만) — 메타데이터는 해시 대상 밖
- `confidence`: HIGH|MEDIUM|LOW — 피처 스냅샷 없으면 LOW + `dataSufficiency` 생략
- `contributions`: 동의 카테고리에 `contributions` 포함 시에만 존재

## POST /fi/decision/evaluate — 정책 판정·대출 조건 산출

동의 게이트를 통과한 점수를 기관이 등록해 둔 정책 규칙과 대출 조건표에 대입해
`APPROVE` / `REVIEW` / `DECLINE` 판정과 조건을 반환합니다. 점수 조회 경로를 그대로 재사용하므로
동의·접근로그 규칙은 `/fi/score/query`와 동일합니다(동의 없으면 같은 방식으로 거부).

요청:

```json
{
  "pairwiseSub": "b41c9d2f-8e77-4a10-9c3e-example",
  "purpose": "UNDERWRITING",
  "productCode": "MICRO_LOAN_12M"
}
```

| 필드 | 필수 | 제약 |
|---|---|---|
| `pairwiseSub` | 필수 | 파트너 전용 익명 ID |
| `purpose` | 필수 | 조회 목적. 동의는 (클라이언트, 목적) 단위 |
| `productCode` | 필수 | `[A-Z0-9_]` 1~40자. 위반 시 `SW1001` |

응답 200 — 승인:

```json
{
  "success": true,
  "data": {
    "pairwiseSub": "b41c9d2f-8e77-4a10-9c3e-example",
    "productCode": "MICRO_LOAN_12M",
    "segmentKey": "KR:MICRO_LOAN_12M",
    "decision": "APPROVE",
    "score": 712,
    "tier": "GOOD",
    "confidence": "HIGH",
    "terms": {
      "depositRatePct": 10.00,
      "creditLimit": 3000000,
      "currency": "KRW",
      "maxTenorMonths": 12,
      "interestAprPct": 14.50
    },
    "firedRules": [],
    "reasonCodes": [
      {
        "code": "RC_LOW_UTILITY_PAYMENT_RELIABILITY",
        "labelEn": "Low utility bill payment reliability",
        "labelLocalized": "전기·수도 등 공과금을 제때 낸 기록이 부족함",
        "category": "PAYMENT",
        "impactPoints": 38,
        "rank": 1
      }
    ],
    "modelVersion": "scorecard-v2.1",
    "scoredAt": "2026-08-20 14:32:11",
    "environment": "LIVE"
  },
  "message": "Decision evaluated"
}
```

응답 200 — 즉시 거절(`terms` 생략):

```json
{
  "success": true,
  "data": {
    "pairwiseSub": "b41c9d2f-8e77-4a10-9c3e-example",
    "productCode": "MICRO_LOAN_12M",
    "segmentKey": "GLOBAL",
    "decision": "DECLINE",
    "score": 412,
    "tier": "POOR",
    "confidence": "MEDIUM",
    "firedRules": [
      { "ruleCode": "KO_SCORE_FLOOR", "ruleType": "KNOCKOUT", "messageEn": "Score below product floor" }
    ],
    "reasonCodes": [
      {
        "code": "RC_CASHFLOW_INSTABILITY",
        "labelEn": "Cash-flow instability",
        "labelLocalized": "수입·지출 흐름이 불안정함",
        "category": "FINANCIAL",
        "impactPoints": 52,
        "rank": 1
      }
    ],
    "modelVersion": "scorecard-v2.1",
    "scoredAt": "2026-08-20 14:32:11",
    "environment": "LIVE"
  },
  "message": "Decision evaluated"
}
```

| 필드 | 설명 |
|---|---|
| `decision` | `APPROVE` / `REVIEW`(수동 심사) / `DECLINE` |
| `segmentKey` | 적용된 세그먼트(`KR:MICRO_LOAN_12M`, `KR`, `GLOBAL` 등) |
| `terms` | 채택된 조건. `DECLINE`이거나 매칭 행이 없으면 생략 |
| `terms.depositRatePct` | 보증금(선납금) 비율 % |
| `terms.creditLimit` / `terms.currency` | 한도 금액 / 3자리 통화코드 |
| `terms.maxTenorMonths` | 최대 대출 기간(개월) |
| `terms.interestAprPct` | 연이율(APR) % |
| `firedRules` | 발화 규칙(`ruleCode`, `ruleType`=KNOCKOUT\|REVIEW, `messageEn`). 없으면 빈 배열 |
| `score`·`tier`·`confidence`·`reasonCodes`·`modelVersion`·`scoredAt` | `/fi/score/query`와 동일한 값 |
| `environment` | LIVE \| SANDBOX |

### 판정 순서 (결정적)

1. 동의 확인 + 점수 조회 — `/fi/score/query`와 동일 경로. 동의 없으면 같은 방식으로 거부.
2. 세그먼트 체인: `{국가}:{productCode}` → `{국가}` → `GLOBAL`(구체적인 것 우선). 국가 컨텍스트가 없으면 `GLOBAL`만.
3. 즉시 거절 규칙(KNOCKOUT): 등록 순서대로 평가, **첫 매치에서 즉시 `DECLINE`**. 조건표는 보지 않으며 `terms` 없이 해당 규칙 1건만 반환.
4. 수동 심사 규칙(REVIEW): 매칭된 규칙을 모두 `firedRules`에 누적(여기서 반환하지 않음).
5. 대출 조건표: 세그먼트 체인 순서로 **기관 전용 행 우선 → 없으면 공용 행**. 조건은 `minScore ≤ score ≤ maxScore` 이고 `confidence ≥ minConfidence`(LOW < MEDIUM < HIGH). 첫 매치 채택.
6. **조건표 미구성/미매치 → `REVIEW`**(조건 없이 수동 심사로 안전 처리 — 임의 승인하지 않음).
7. 채택 행의 판정 사용. 단 REVIEW 규칙이 하나라도 발화한 상태에서 행이 `APPROVE`면 `REVIEW`로 강등. 행이 `DECLINE`이면 `terms` 생략.

규칙·조건표는 운영팀이 기관별로 등록합니다(관리자 API `/admin/hacs/fi-policy-rules`, `/admin/hacs/fi-terms`).
기관 전용 설정이 없으면 공용 설정이 적용됩니다.

### 규칙 조건 항목

| field | 연산자 | 값 |
|---|---|---|
| `score` | LT·LTE·GT·GTE·EQ·NEQ | 숫자 |
| `coveragePercent` | LT·LTE·GT·GTE·EQ·NEQ | 숫자(`dataSufficiency.coveragePercent`) |
| `tier` | EQ·NEQ는 이름 비교, 나머지는 등급 순서(POOR < BELOW_AVERAGE < FAIR < GOOD < EXCELLENT) | 등급 문자열 |
| `confidence` | EQ·NEQ는 이름 비교, 나머지는 LOW < MEDIUM < HIGH 순서 | LOW·MEDIUM·HIGH |
| `reasonCode` | CONTAINS·EQ·NEQ | 사유코드 문자열 |

한 규칙 안의 조건은 모두 만족해야 발화합니다(AND). 형식이 잘못된 규칙은 매치되지 않고 건너뜁니다.

### 샌드박스

점수는 합성값(샌드박스 고정 주체 표와 동일)이고, **규칙과 조건표는 실제 등록된 기관 설정이 그대로 적용**됩니다.
샌드박스 합성 점수에는 국가 컨텍스트가 없어 세그먼트 체인은 `GLOBAL` 하나뿐입니다.
`SANDBOX-NO-CONSENT`·`SANDBOX-EXPIRED`는 점수 조회와 동일하게 거부됩니다.

```bash
curl -s https://trendy.storydot.kr/superapp/api/fi/decision/evaluate \
  -H "X-Client-Id: YOUR_CLIENT_ID" \
  -H "X-API-Key: YOUR_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "pairwiseSub": "SANDBOX-TIER-C", "purpose": "UNDERWRITING", "productCode": "MICRO_LOAN_12M" }'
```

사용량 집계에서는 `DECISION` 버킷, 호출당 2유닛입니다.

## POST /fi/score/batch — 배치 조회 (1~100건)

요청:

```json
{ "subjects": ["b41c9d2f-8e77-4a10-9c3e-example", "SANDBOX-NO-CONSENT"], "purpose": "ACCOUNT_REVIEW" }
```

응답 200:

```json
{
  "success": true,
  "data": {
    "batchId": "3f8a1b62-90cd-4e5f-a217-7c9d1e2f3a4b",
    "summary": { "requested": 2, "returned": 1, "denied": 1 },
    "results": [
      {
        "pairwiseSub": "b41c9d2f-8e77-4a10-9c3e-example",
        "status": "OK",
        "score": 712,
        "tier": "GOOD",
        "reasonCodes": [],
        "modelVersion": "scorecard-v2.1",
        "scoredAt": "2026-08-20 14:32:11",
        "decisionHash": "9f2ab1c4e5d6...64hex"
      },
      { "pairwiseSub": "SANDBOX-NO-CONSENT", "status": "DENIED" }
    ]
  },
  "message": "Batch completed"
}
```

NO_CONSENT/REVOKED/EXPIRED/NOT_FOUND는 전부 단일 라벨 `DENIED`(타이밍 균일화). 100건 초과 시 `SW1001`.

## POST /fi/verify — 신용등급 증명서(어테스테이션) 검증

동의 불필요. `vcId`/`reputationHash` 중 1개 이상 필수, `tierCommitment`/`tier`는 선택 교차검증.
비정상 증명서도 200 + `valid=false`.

```json
{ "vcId": "vc-8c3a51d0-example", "reputationHash": "0x4e5f6a7b...", "tierCommitment": "0x1a2b3c...", "tier": "GOOD" }
```

```json
{
  "success": true,
  "data": {
    "valid": true,
    "reason": "VALID",
    "tier": "GOOD",
    "status": "ANCHORED",
    "issuedAt": "2026-07-01 10:00:00",
    "expiresAt": "2026-09-29 10:00:00",
    "anchoredTxHash": "0x7d1e...",
    "modelVersion": "scorecard-v2.1"
  },
  "message": "Attestation verification completed"
}
```

`reason`: VALID | NOT_FOUND | REVOKED | EXPIRED(90일 경과) | COMMITMENT_MISMATCH | TIER_MISMATCH

## GET /fi/score/history — 점수 이력

`GET /fi/score/history?pairwiseSub=&fromDate=&toDate=` (최대 365일, 동의 카테고리 `history` 필수)

```json
{
  "success": true,
  "data": {
    "pairwiseSub": "b41c9d2f-8e77-4a10-9c3e-example",
    "entries": [
      { "score": 712, "tier": "GOOD", "modelVersion": "scorecard-v2.1", "scoredAt": "2026-08-20 14:32:11", "decisionHash": "9f2ab1c4e5d6...64hex", "anchorTxHash": "0x7d1e..." }
    ]
  },
  "message": "History retrieved"
}
```

## GET /fi/whoami — 내 클라이언트 확인 (인증만, 동의 불필요)

```json
{ "success": true, "data": { "clientId": "bank-busan-001", "name": "Example Bank", "environment": "SANDBOX", "rateLimitTier": "STANDARD", "status": "ACTIVE" }, "message": "OK" }
```

## GET /fi/usage — 사용량 자기조회

`GET /fi/usage?fromDate=&toDate=` (최대 92일). 유닛 가중치: score/query=1, batch 건당=1, verify=1, history=2, decision=2.
`endpoint` 값: QUERY | BATCH | VERIFY | HISTORY | DECISION. 집계는 매일 03:30 전일분.

```json
{
  "success": true,
  "data": {
    "clientId": "bank-busan-001",
    "days": [
      { "date": "2026-08-20", "endpoint": "QUERY", "callCount": 41, "unitCount": 41 },
      { "date": "2026-08-20", "endpoint": "BATCH", "callCount": 2, "unitCount": 130 },
      { "date": "2026-08-20", "endpoint": "DECISION", "callCount": 9, "unitCount": 18 }
    ]
  },
  "message": "Usage retrieved"
}
```

## POST /fi/consent/requests — 동의 요청

```json
{ "pairwiseSub": "b41c9d2f-8e77-4a10-9c3e-example", "purpose": "UNDERWRITING" }
```

응답 200: `data=null`, `message="Consent request sent"`

## GET /fi/consent/{consentReceiptId} — 동의 상태

```json
{
  "success": true,
  "data": {
    "consentReceiptId": "cr-1f2e3d4c-example",
    "pairwiseSub": "b41c9d2f-8e77-4a10-9c3e-example",
    "clientId": "bank-busan-001",
    "purpose": "UNDERWRITING",
    "scope": "score",
    "dataCategories": "score,tier,reason-codes",
    "status": "ACTIVE",
    "grantedAt": "2026-08-01 11:20:00",
    "expiresAt": "2026-11-01 11:20:00"
  },
  "message": "Consent status retrieved"
}
```

`status`: ACTIVE | REVOKED | EXPIRED. `dataCategories` 기본값 `score,tier,reason-codes`,
선택 확장 `contributions`, `history`, `monitoring`.

## GET /fi/health — 공개 상태 확인

```json
{ "success": true, "data": { "status": "UP", "pullTypes": "SOFT" }, "message": "OK" }
```

## 관리자(운영) 전용 — 관리자 JWT(ADMIN) 필요

| 메서드·경로 | 설명 |
|---|---|
| `GET·POST /api/v1/admin/hacs/fi-policy-rules`, `PUT·DELETE .../fi-policy-rules/{id}` | 판정 규칙(KNOCKOUT·REVIEW) CRUD — `/fi/decision/evaluate`가 사용하는 설정 |
| `GET·POST /api/v1/admin/hacs/fi-terms`, `PUT·DELETE .../fi-terms/{id}` | 대출 조건표 행 CRUD |
| `GET /api/v1/admin/hacs/fi-usage?clientId=&fromDate=&toDate=` | 클라이언트별 사용량 |
| `POST /api/v1/admin/hacs/fi-usage/aggregate?date=` | 수동 재집계 |
| `GET /api/v1/admin/hacs/fi-webhooks` | 웹훅 목록 |
| `POST /api/v1/admin/hacs/fi-webhooks` | 생성(url, events, clientId) — 시크릿 1회 평문 반환 |
| `PUT /api/v1/admin/hacs/fi-webhooks/{id}` | 상태 변경·재활성(연속 실패 리셋) |
| `DELETE /api/v1/admin/hacs/fi-webhooks/{id}` | 삭제 |
| `POST /api/v1/admin/hacs/fi-webhooks/{id}/test` | 테스트 이벤트 발송 |
| `GET /api/v1/admin/hacs/fi-webhooks/{id}/deliveries` | 최근 전달 50건 |
| `GET /api/v1/admin/hacs/fi-access-logs/export?clientId=&fromDate=&toDate=` | 접근로그 CSV (accessedAt,clientId,subjectPairwiseSub,purpose,outcome,returnedTier,modelVersion,environment,batchId,entryHash) |
| `GET /api/v1/admin/hacs/fi-consents/export?clientId=` | 동의 CSV (grantedAt,revokedAt,clientId,pairwiseSub,purpose,dataCategories,status,receiptId) |
