---
title: Java·Kotlin SDK
description: 페이싱크 Java SDK와 Kotlin 확장으로 주문, 입금, 현금영수증 API를 호출하고 웹훅 서명을 검증하는 방법이에요.
sidebar:
  icon: coffee
---

Java SDK는 v2 API의 주문, 입금, 현금영수증 API와 웹훅 서명 검증을 지원해요. Kotlin에서는 코루틴과 `Flow`를 쓰는 확장 모듈을 함께 쓸 수 있어요.

| 모듈 | 설명 |
| --- | --- |
| `kr.paysync:paysync-sdk` | Java SDK예요. Java 17 이상에서 써요. |
| `kr.paysync:paysync-sdk-kotlin` | Kotlin 확장이에요. Kotlin 2.2 이상에서 써요. Java SDK도 함께 들어와요. |

## 설치

```kotlin Gradle (Kotlin)
repositories {
    mavenCentral()
    maven("https://forgejo.verchase.com/api/packages/paysync/maven")
}

dependencies {
    implementation("kr.paysync:paysync-sdk:1.0.0")
    // Kotlin이라면 대신 이것을 넣어요.
    implementation("kr.paysync:paysync-sdk-kotlin:1.0.0")
}
```

```xml Maven
<repositories>
  <repository>
    <id>paysync</id>
    <url>https://forgejo.verchase.com/api/packages/paysync/maven</url>
  </repository>
</repositories>

<dependency>
  <groupId>kr.paysync</groupId>
  <artifactId>paysync-sdk</artifactId>
  <version>1.0.0</version>
</dependency>
```

## 클라이언트 만들기

API 키는 [API 키](/api-keys)를 따라 준비해요. 클라이언트는 하나만 만들어 같이 쓰세요.

```java Java
PaySyncClient client = PaySyncClient.create(System.getenv("PAYSYNC_API_KEY"));
```

```kotlin Kotlin
val client = SuspendingPaySyncClient(System.getenv("PAYSYNC_API_KEY")) {
    timeout(10.seconds)
}
```

빌더로 설정을 바꿀 수 있어요.

| 설정 | 기본값 | 설명 |
| --- | --- | --- |
| `apiKey` | | API 키예요. 필수예요. |
| `baseUrl` | `https://api.paysync.kr` | API 서버 주소예요. |
| `timeout` | 30초 | 응답을 기다리는 시간이에요. |
| `connectTimeout` | 10초 | 연결을 기다리는 시간이에요. |
| `maxRetries` | `2` | 조회 요청이 실패했을 때 다시 시도할 횟수예요. |
| `appInfo` | | `User-Agent` 헤더에 붙일 애플리케이션 이름이에요. |

## API

| 서비스 | 메서드 | API |
| --- | --- | --- |
| `invoices()` | `create`, `get`, `list`, `listAll`, `update`, `markPaid`, `cancel` | [주문](/api-reference/invoices/issue) |
| `transactions()` | `get`, `list`, `listAll` | [입금](/api-reference/transactions/list) |
| `cashReceipts()` | `issue`, `get`, `list`, `listAll`, `cancel`, `reissue`, `urls`, `sendEmail` | [현금영수증](/api-reference/cash-receipts/issue) |

Java에서는 메서드마다 `CompletableFuture`를 돌려주는 `...Async` 버전이 있어요. Kotlin의 `SuspendingPaySyncClient`에서는 모두 `suspend` 함수예요.

### 주문 만들기

```java Java
Invoice invoice = client.invoices().create(InvoiceCreateParams.builder()
        .customerName("홍길동")
        .customerEmail("hong@example.com")
        .amount(50_000)
        .expireAfterDays(3)
        .putMetadata("orderId", "ORDER-2026-0001")
        .build());

System.out.println(invoice.guideUrl());
```

```kotlin Kotlin
val invoice = client.invoices.create {
    customerName("홍길동")
    customerEmail("hong@example.com")
    amount(50_000)
    expireAfterDays(3)
    putMetadata("orderId", "ORDER-2026-0001")
}
```

### 주문 수정

넣은 값만 바뀌어요. 값을 지우려면 `clearCustomerEmail()`, `clearCustomerPhoneNumber()`, `clearExpiresAt()`, `clearCashReceipt()`를 써요.

```java Java
client.invoices().update(invoice.id(), InvoiceUpdateParams.builder()
        .amount(55_000)
        .clearCustomerPhoneNumber()
        .resendGuideEmail(true)
        .build());
```

```kotlin Kotlin
client.invoices.update(invoice.id()) {
    amount(55_000)
    clearCustomerPhoneNumber()
    resendGuideEmail(true)
}
```

### 목록 조회

`list`는 한 페이지를 돌려주고, `listAll`은 조건에 맞는 항목을 마지막 페이지까지 모두 조회해요. Java에서는 `Iterable`, Kotlin 코루틴 클라이언트에서는 `Flow`예요.

```java Java
Page<Invoice> page = client.invoices().list(InvoiceListParams.builder()
        .status(InvoiceStatus.PENDING)
        .dateFrom(LocalDate.of(2026, 4, 1))
        .limit(50)
        .build());

for (Transaction transaction : client.transactions().listAll(TransactionListParams.builder()
        .status(TransactionStatus.UNMATCHED)
        .build())) {
    System.out.println(transaction.depositorName());
}
```

```kotlin Kotlin
val page = client.invoices.list {
    status(InvoiceStatus.PENDING)
    limit(50)
}

client.transactions.listAll { status(TransactionStatus.UNMATCHED) }
    .collect { println(it.depositorName) }
```

### 현금영수증

발급이 거절되면 예외 대신 `status`가 `FAILED`인 현금영수증이 와요. 이유는 `failure()`에서 확인해요.

```java Java
CashReceipt receipt = client.cashReceipts().issue(CashReceiptIssueParams.builder()
        .type(CashReceiptType.PERSONAL)
        .identifier("01012345678")
        .amount(50_000)
        .invoiceId(invoice.id())
        .build());

if (receipt.status() == CashReceiptStatus.FAILED) {
    System.out.println(receipt.failure().message());
}
```

## 응답 값

- 시각은 모두 `Instant`예요.
- 상태, 은행, 매칭 방식 같은 값은 enum이에요. SDK가 모르는 새 값은 `UNKNOWN`으로 와요.
- 모델은 Java record예요. Kotlin에서는 `invoice.id()`나 `invoice.customer().name`처럼 써요.

## 오류 처리

페이싱크가 오류를 응답하면 `PaySyncApiException`을 던져요. [에러 코드](/api-reference/error-codes)는 `code()`로 구분하고, 자주 쓰는 값은 `ErrorCode`에 있어요.

| 예외 | HTTP |
| --- | --- |
| `AuthenticationException` | 401 |
| `PermissionDeniedException` | 403 |
| `NotFoundException` | 404 |
| `ConflictException` | 409 |
| `InvalidRequestException` | 400, 422 등 |
| `RateLimitException` | 429 |
| `ServerException` | 5xx |
| `PaySyncConnectionException` | 연결 실패, 응답 시간 초과 |

일부 오류는 `data`에 처리에 필요한 값을 담아요. SDK에서는 메서드로 꺼내요.

| 메서드 | `code` |
| --- | --- |
| `duplicateInvoice()` | `220010` |
| `cancelBlocked()` | `240006` |
| `failedCashReceipt()` | `240008` |
| `providerError()` | `240009` |
| `existingCashReceiptId()` | `240012` |

```java Java
try {
    client.invoices().create(params);
} catch (ConflictException e) {
    if (ErrorCode.INVOICE_ALREADY_EXISTS.equals(e.code())) {
        String existingId = e.duplicateInvoice().invoiceId();
    }
}
```

## 웹훅

웹훅 서명 키(`whsec_...`)로 `Webhook`을 만들고 `verify`에 받은 본문과 헤더를 넣어요. 본문은 파싱하기 전의 문자열 그대로 넣어야 해요. 서명이 맞지 않으면 `WebhookVerificationException`을 던져요.

```java Java (Spring)
private final Webhook webhook = new Webhook(System.getenv("PAYSYNC_WEBHOOK_SECRET"));

@PostMapping("/paysync/webhook")
public ResponseEntity<Void> handle(@RequestBody String rawBody, @RequestHeader HttpHeaders headers) {
    WebhookEvent event;
    try {
        event = webhook.verify(rawBody, headers.asMultiValueMap());
    } catch (WebhookVerificationException e) {
        return ResponseEntity.badRequest().build();
    }

    switch (event) {
        case WebhookEvent.InvoiceEvent e when e.type() == WebhookEventType.INVOICE_PAID -> fulfill(e.invoice());
        case WebhookEvent.InvoiceEvent e -> { }
        case WebhookEvent.TransactionEvent e -> { }
        case WebhookEvent.UnknownEvent e -> { }
    }
    return ResponseEntity.ok().build();
}
```

```kotlin Kotlin
val webhook = Webhook(System.getenv("PAYSYNC_WEBHOOK_SECRET"))

when (val event = webhook.verify(rawBody, headers)) {
    is WebhookEvent.InvoiceEvent -> if (event.type() == WebhookEventType.INVOICE_PAID) fulfill(event.invoice())
    is WebhookEvent.TransactionEvent -> log(event.transaction())
    is WebhookEvent.UnknownEvent -> Unit
}
```

Java 17에서는 `switch (event.type())`로 나눠요. 같은 이벤트가 여러 번 올 수 있으니 `event.id()`로 중복을 거르세요. 응답 규칙과 재시도는 [웹훅 개요](/api-reference/webhooks/overview)를 참고하세요.
