<!-- Source: https://docs.infraio.xyz/ko/api-reference/authentication -->
<!-- Last updated: 2026-10-04 -->

# 인증

InfraIO Pay에는 인증 모델이 다른 **두 가지 API 표면**이 있습니다. 호출자가
누구인지에 맞는 것을 선택하세요.

| 표면 | 경로 프리픽스 | 대상 | 인증 |
| --- | --- | --- | --- |
| **가맹점 B2B** | `/b2b/v1/*` | 가맹점 서버 | HMAC-SHA256 요청 서명 |
| **대시보드** | 가맹점 대시보드에서 사용 | 가맹점 대시보드 브라우저 세션 | Bearer JWT |

이 페이지는 **B2B** 표면을 다룹니다. API 키 쌍으로 가맹점 서버에서 호출하는
표면입니다. 대시보드 표면은 InfraIO Pay 가맹점 대시보드에서 사용하며 공개 연동용
표면이 아닙니다.

항상 `/b2b` 프리픽스를 포함한 전체 경로로 요청을 보내고, 같은 경로로 서명하세요
(아래 참조).

## 엔드포인트

| 환경 | 기본 URL |
| --- | --- |
| 테스트 | `https://api-dev.infraio.xyz` |
| 라이브 | `https://api.infraio.xyz` |

동일한 URL 패턴 — 환경은 URL이 아닌 **키 프리픽스**(`pk_test_…` vs `pk_live_…`)로
제어됩니다.

## 키 쌍

가맹점 대시보드(**Developers → API keys → + Add key**)에서 두 가지 값을 받습니다.

- **공개 키**(`pk_test_…` 또는 `pk_live_…`) — 계정을 식별합니다.
  `X-Client-ID`로 전송됩니다. 브라우저 번들에 임베드해도 안전합니다(SDK가
  이미 그렇게 합니다).
- **비밀 키**(`sk_test_…` 또는 `sk_live_…`) — HMAC 서명 키. 서버 전용입니다.
  데이터베이스 패스워드처럼 다루세요.

> **Important:**
>
> 비밀 키가 브라우저 번들, git 저장소, 로그 라인, 또는 공유 채팅에 노출된
> 경우 — 대시보드에서 **즉시 폐기**하세요.
> 폐기는 즉시 적용되며 오버랩 윈도우는 없습니다. 새 키를 발급하고 재배포하세요.

## 요청 서명

`/b2b/v1/*`에 대한 모든 호출은 세 개의 헤더를 포함합니다.

```http
X-Client-ID:  pk_live_70d0becbd7a53047b0dfe88a960d10ad
X-Timestamp:  1715990400
X-Signature:  9a8b7c6d…             (hex HMAC-SHA256)
```

서명은 정규 문자열에 대해 계산됩니다.

```
METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + BODY
```

- `METHOD` — 대문자 HTTP 동사(`POST`, `GET`, …).
- `PATH` — 호스트 없이, 쿼리 문자열 없이 **`/b2b` 프리픽스가 포함된**
  요청 경로(예: `/b2b/v1/checkout-sessions/quick`). 프리픽스가
  반드시 있어야 합니다. 쿼리 매개변수는 서명되지 **않습니다** — `GET
  …?cursor=…&limit=20`의 경우, `?…` 부분이 아닌 경로만 서명하세요.
- `TIMESTAMP` — 10진수 문자열로 된 유닉스 초(예: `"1715990400"`),
  `X-Timestamp`와 정확히 일치해야 함.
- `BODY` — 원본 요청 본문 바이트. `GET`/`DELETE`의 경우 빈 문자열.

**비밀** 키로 HMAC-SHA256 서명, **hex**로 출력:

**Node / TS**

```ts
import { createHmac } from "node:crypto";

function sign({ method, path, body, secret }: {
  method: string; path: string; body: string; secret: string;
}) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const input = [method.toUpperCase(), path, timestamp, body].join("\n");
  const signature = createHmac("sha256", secret).update(input).digest("hex");
  return { timestamp, signature };
}
```

**Go**

```go
package infraio

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strconv"
    "time"
)

func Sign(method, path, body, secret string) (timestamp, signature string) {
    timestamp = strconv.FormatInt(time.Now().Unix(), 10)
    input := method + "\n" + path + "\n" + timestamp + "\n" + body
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(input))
    return timestamp, hex.EncodeToString(mac.Sum(nil))
}
```

**Python**

```python
import hmac, hashlib, time

def sign(method: str, path: str, body: str, secret: str):
    timestamp = str(int(time.time()))
    input_ = f"{method}\n{path}\n{timestamp}\n{body}"
    signature = hmac.new(
        secret.encode(), input_.encode(), hashlib.sha256
    ).hexdigest()
    return timestamp, signature
```

## 왜 HMAC이고 Bearer가 아닌가?

베어 Bearer 토큰 API는 모든 요청에서 유일한 시크릿을 회선을 통해 전송합니다.
TLS-종료 프록시 로그 하나를 캡처한 누구나 계정 키를 얻게 됩니다. HMAC 서명은
시크릿이 절대 전송되지 않음을 의미합니다 — 일회용인 파생 서명(해당 정확한
요청 + 해당 정확한 분에 바인딩됨)만 전송됩니다.

트레이드오프: 모든 호출에 대해 서명을 계산해야 합니다. 아직 서버 SDK는
없지만, 위의 헬퍼는 언어당 약 15줄입니다.

## 타임스탬프 허용 오차

허용 오차는 **±5분**(300초)입니다. 이 범위를 벗어난 요청은
`401 invalid_signature`로 거부됩니다. 두 가지
시사점:

1. **NTP로 서버 클록을 동기화하세요.** 드리프트된 클록을 가진 장시간 실행
   크론은 간헐적으로 실패합니다.
2. **서명을 미리 계산하여 큐에 넣지 마세요.** 요청이 재시도 큐에 5분
   이상 머무르면 서명이 만료됩니다.

## 키 스코프

비밀 키는 다음 스코프 번들 중 하나 이상을 가집니다.

| 스코프 | 의도된 사용 |
| --- | --- |
| `read` | 주문, 세션, 환불 목록/읽기 |
| `write_order` | 체크아웃 세션, 주문 생성 |
| `write_refund` | 환불 발행, 환불 요청 토큰 발급 |
| `webhook_manage` | 웹훅 엔드포인트 생성/업데이트/삭제 |

대시보드는 기본적으로 "전체 액세스" 키(네 가지 스코프 모두)를 발급합니다.
**Developers → API keys → + Add key**에서 통합이 필요한 스코프만 체크하여
제한된 스코프 키를 발급할 수 있습니다.

> **Warning:**
>
> **스코프는 아직 강제되지 않습니다.** 스코프는 키에 기록되고 대시보드에
> 표시되지만, 유효한 `sk_…` 키라면 가맹점의 모든 `/b2b/v1/*` 엔드포인트를
> 호출할 수 있습니다. 스코프를 보안 경계로 의존하지 마세요. 액세스를 제한하려면
> 키를 로테이션하거나 폐기하세요.

## 검증 실패

서명, `X-Client-ID`, 또는 타임스탬프가 유효하지 않으면 요청은 API에 도달하기 전에
**401 `INVALID_SIGNATURE`**로 거부됩니다. `/b2b/v1/*` 요청만 이 방식으로
서명됩니다. 웹훅은 별도의 방식을 사용합니다(아래 참조).

## 다음 단계

- [오류](https://docs.infraio.xyz/ko/api-reference/errors) — 4xx/5xx 응답 형식.
- [보안 → API 키](https://docs.infraio.xyz/ko/security/api-keys) — 로테이션, 폐기, 시크릿 유출 시
  조치.
- [웹훅 → 서명 검증](https://docs.infraio.xyz/ko/webhooks/signature-verification) — *다른* HMAC
  방식을 사용합니다(헤더 `X-Signature: sha256=…`, `X-Timestamp + "." +
  raw_body` 서명, 24시간 로테이션 grace 윈도우 동안 선택적 `X-Signature-Prev`).
  방식을 혼동하지 마세요 — 해시 알고리즘은 같지만 서명된 바이트와 시크릿
  패밀리(`whsec_…` vs `sk_…`)가 다릅니다.
