---
title: API 개요
description: 이전 버전인 v1 API의 기본 주소, 인증 방식, 응답 형식, 페이지네이션, ID 형식과 v2 전환 뒤 달라진 동작을 정리했어요.
sidebar:
  label: 개요
  order: 1
---

v1 API는 예전에 연동한 서버가 그대로 동작하도록 남겨 둔 이전 버전이에요. 경로, 요청 본문, 문자열 `code`, ISO 8601 시각 형식이 예전과 같아요. 새로 연동한다면 [v2 API](/api-reference)를 쓰세요. v2에는 주문 수정과 취소, 현금영수증 재발급과 부분 취소, 새 웹훅 이벤트가 있어요.

:::note
v1 API는 계속 동작해요. 같은 API 키로 v1과 v2를 함께 호출할 수 있어서 엔드포인트를 하나씩 옮겨도 돼요.
:::

## 기본 정보

| 항목 | 값 |
| --- | --- |
| 기본 주소 | `https://api.paysync.kr` |
| 버전 | `v1` (모든 경로가 `/v1`으로 시작해요) |
| 인증 | `Authorization: Bearer <API 키>` |
| 본문 형식 | `application/json`, UTF-8 |
| 시각 형식 | ISO 8601, UTC. 예: `2026-04-28T03:14:15.000+00:00` |
| 날짜 필터 형식 | `YYYY-MM-DD`, 한국 시간(KST) 기준 |
| OpenAPI 명세 | [`/openapi.json`](https://docs.paysync.kr/openapi.json) |

## v2 전환 뒤 달라진 점

페이싱크 v2에서 계정 대신 워크스페이스가 데이터를 가져요. v1 API는 API 키가 속한 워크스페이스의 데이터를 다뤄요. 응답 형식은 그대로지만 아래 동작이 바뀌었어요.

| 항목 | 지금 동작 |
| --- | --- |
| `issuerId`, `ownerId` | 워크스페이스 소유자의 계정 ID(`acc_...`)예요. |
| 시각 | 초 단위로 저장해서 밀리초는 항상 `.000`이에요. 시간대 표기는 `Z` 대신 `+00:00`이에요. |
| `offset`, `limit` 누락 | `422 INVALID_OFFSET` 또는 `422 INVALID_LIMIT`가 와요. |
| 잘못된 JSON, 목록에 없는 열거형 값 | `400 BAD_REQUEST`가 `code`와 `data` 형식으로 와요. |
| 읽기 전용 API 키로 쓰기 요청 | `403 INSUFFICIENT_PERMISSIONS`가 와요. |
| 주문 삭제 | 주문을 지우지 않고 취소 상태로 바꿔요. 취소된 주문은 v1에서 보이지 않아요. |
| 주문의 `cashReceipt` | 주문을 만들 때 `identifier` 형식을 검사해요. |
| 현금영수증 발급 취소 | 국세청 발급이 끝난 `ISSUED` 현금영수증만 취소할 수 있어요. |
| `matchMethod` | 새 매칭 규칙 값이 더 나올 수 있어요. |
| 웹훅 | 실패하면 6번 더 보내고, `2xx` 응답을 모두 성공으로 봐요. |

## 인증

대시보드 [API 키](https://paysync.kr/dashboard/api-keys)에서 발급한 키를 `Authorization` 헤더에 넣어요. API 키는 `sk_live_`로 시작해요.

```http
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
```

만료된 키, 삭제된 키, 대시보드에서 끈 키로 요청하면 `401 NOT_AUTHORIZED`가 와요. 대시보드 로그인 토큰으로는 v1 API를 호출할 수 없고 `403 INSUFFICIENT_PERMISSIONS`가 와요.

주문 하나를 만드는 요청이에요.

```bash cURL
curl -X POST https://api.paysync.kr/v1/invoices \
  -H "Authorization: Bearer $PAYSYNC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "bankAccountIds": [],
    "customer": {
      "name": "홍길동",
      "email": "hong@example.com",
      "phoneNumber": "01012345678"
    },
    "amount": 50000,
    "expireAfter": "1d",
    "metadata": { "orderId": "ORDER-2026-0001" }
  }'
```

```js Node.js
const res = await fetch("https://api.paysync.kr/v1/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PAYSYNC_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    bankAccountIds: [],
    customer: {
      name: "홍길동",
      email: "hong@example.com",
      phoneNumber: "01012345678",
    },
    amount: 50000,
    expireAfter: "1d",
    metadata: { orderId: "ORDER-2026-0001" },
  }),
});
const { code, data } = await res.json();
```

```python Python
import os
import requests

res = requests.post(
    "https://api.paysync.kr/v1/invoices",
    headers={"Authorization": f"Bearer {os.environ['PAYSYNC_API_KEY']}"},
    json={
        "bankAccountIds": [],
        "customer": {
            "name": "홍길동",
            "email": "hong@example.com",
            "phoneNumber": "01012345678",
        },
        "amount": 50000,
        "expireAfter": "1d",
        "metadata": {"orderId": "ORDER-2026-0001"},
    },
)
print(res.json())
```

```kotlin Kotlin
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.*

val client = HttpClient(CIO) {
    install(ContentNegotiation) { json() }
}

val response = client.post("https://api.paysync.kr/v1/invoices") {
    bearerAuth(System.getenv("PAYSYNC_API_KEY"))
    contentType(ContentType.Application.Json)
    setBody(buildJsonObject {
        putJsonArray("bankAccountIds") {}
        putJsonObject("customer") {
            put("name", "홍길동")
            put("email", "hong@example.com")
            put("phoneNumber", "01012345678")
        }
        put("amount", 50000)
        put("expireAfter", "1d")
        putJsonObject("metadata") { put("orderId", "ORDER-2026-0001") }
    })
}

println(response.bodyAsText())
```

```java Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class IssueInvoice {

    public static void main(String[] args) throws Exception {
        String body = """
            {
              "bankAccountIds": [],
              "customer": {
                "name": "홍길동",
                "email": "hong@example.com",
                "phoneNumber": "01012345678"
              },
              "amount": 50000,
              "expireAfter": "1d",
              "metadata": { "orderId": "ORDER-2026-0001" }
            }
            """;

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.paysync.kr/v1/invoices"))
            .header("Authorization", "Bearer " + System.getenv("PAYSYNC_API_KEY"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.body());
    }
}
```

```rust Rust
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let api_key = std::env::var("PAYSYNC_API_KEY").expect("PAYSYNC_API_KEY");

    let body = json!({
        "bankAccountIds": [],
        "customer": {
            "name": "홍길동",
            "email": "hong@example.com",
            "phoneNumber": "01012345678"
        },
        "amount": 50000,
        "expireAfter": "1d",
        "metadata": { "orderId": "ORDER-2026-0001" }
    });

    let response = reqwest::Client::new()
        .post("https://api.paysync.kr/v1/invoices")
        .bearer_auth(api_key)
        .json(&body)
        .send()
        .await?
        .text()
        .await?;

    println!("{response}");
    Ok(())
}
```

:::warning
API 키 값은 발급할 때 한 번만 보여요. 키를 잃어버렸다면 새로 발급하고 이전 키를 삭제하세요. 키를 브라우저 코드나 공개 저장소에 넣지 마세요.
:::

## API 키 계좌 제한

API 키를 만들 때 **계좌 범위**를 **고른 계좌만**으로 정하면 그 키로 다룰 수 있는 주문과 입금이 좁혀져요. **모든 계좌**로 만든 키에는 제한이 없어요.

| API | 계좌를 제한한 키의 동작 |
| --- | --- |
| 주문 생성 | `bankAccountIds`에 허용된 계좌만 넣을 수 있어요. 빈 배열(전체 계좌 수신)로는 만들 수 없어요. |
| 주문 조회, 결제 완료 처리, 삭제 | `bankAccountIds`가 모두 허용된 계좌인 주문만 다뤄요. 전체 계좌 수신 주문은 다룰 수 없어요. |
| 주문 목록 | 위 조건에 맞는 주문만 나와요. |
| 입금 조회와 목록 | 허용된 계좌로 들어온 입금만 나와요. |
| 현금영수증 | 계좌와 관계가 없어서 제한하지 않아요. |

허용되지 않은 주문이나 입금에 접근하면 `403 INSUFFICIENT_PERMISSIONS`가 와요.

## 응답 형식

모든 응답은 `code`와 `data`를 담아요. `code`는 처리 결과를 나타내는 문자열이고, HTTP 상태 코드와 함께 와요.

| 필드 | 타입 | 설명 |
| --- | --- | --- |
| `code` | string | 처리 결과 코드예요. 예: `OK`, `CREATED`, `INVALID_AMOUNT` |
| `data` | object \| array \| null | 응답 데이터예요. 오류 응답에서는 `null`이에요. |

```json 주문 생성 성공
{
  "code": "CREATED",
  "data": {
    "id": "ivc_a1b2c3d4e5f6g7h8i9j0k1l2",
    "issuerId": "acc_x9y8z7w6v5u4t3s2r1q0p9o8",
    "bankAccountIds": [],
    "customer": {
      "name": "홍길동",
      "email": "hong@example.com",
      "phoneNumber": "01012345678"
    },
    "cashReceipt": null,
    "amount": 50000,
    "paid": false,
    "metadata": { "orderId": "ORDER-2026-0001" },
    "issuedAt": "2026-04-28T03:14:15.000+00:00",
    "expiresAt": "2026-04-29T03:14:15.000+00:00",
    "deletedAt": null
  }
}
```

```json 오류
{
  "code": "INVALID_AMOUNT",
  "data": null
}
```

코드 목록은 [에러 코드](/api-reference/v1/error-codes)에 있어요.

## 페이지네이션

목록 조회는 `offset`과 `limit` 쿼리 파라미터로 페이지를 나눠요. 두 값 모두 필수예요. `offset`을 빠뜨리면 `INVALID_OFFSET`, `limit`을 빠뜨리면 `INVALID_LIMIT`가 와요.

| 파라미터 | 타입 | 설명 |
| --- | --- | --- |
| `offset` | integer | 건너뛸 항목 수예요. 0 이상이에요. |
| `limit` | integer | 가져올 항목 수예요. 1~100이에요. |

목록 응답에는 조건에 맞는 전체 항목 수 `totalItems`가 함께 와요. 항목은 최신순으로 정렬돼요.

```json 입금 내역 목록
{
  "code": "OK",
  "totalItems": 142,
  "data": [
    {
      "id": "trx_a1b2c3d4e5f6g7h8i9j0k1l2",
      "matchedInvoiceId": "ivc_n0o1p2q3r4s5t6u7v8w9x0y1",
      "matchMethod": "EXACT",
      "amount": 50000,
      "description": "홍길동"
    }
  ]
}
```

## 날짜 필터

`dateAfter`와 `dateBefore`는 `YYYY-MM-DD` 형식의 한국 시간 날짜예요. 두 날짜 모두 결과에 포함돼요. `dateAfter=2026-04-01&dateBefore=2026-04-30`은 4월 1일 0시부터 4월 30일 24시(KST)까지예요.

## ID 형식

모든 ID는 리소스 접두사 뒤에 영문 소문자와 숫자 24자가 붙어요.

| 접두사 | 리소스 |
| --- | --- |
| `ivc_` | 주문 |
| `trx_` | 입금 |
| `crt_` | 현금영수증 |
| `bac_` | 은행 계좌 |
| `acc_` | 페이싱크 계정 (워크스페이스 소유자) |
| `whd_` | 웹훅 |
| `whm_` | 웹훅 전송 |

## 엔드포인트

| 메서드 | 경로 | 설명 |
| --- | --- | --- |
| `POST` | `/v1/invoices` | [주문 생성](/api-reference/v1/invoices/issue) |
| `GET` | `/v1/invoices` | [주문 목록 조회](/api-reference/v1/invoices/list) |
| `GET` | `/v1/invoices/{id}` | [주문 단건 조회](/api-reference/v1/invoices/get) |
| `POST` | `/v1/invoices/{id}/mark-as-paid` | [주문 결제 완료 처리](/api-reference/v1/invoices/mark-as-paid) |
| `DELETE` | `/v1/invoices/{id}` | [주문 삭제](/api-reference/v1/invoices/delete) |
| `GET` | `/v1/transactions` | [입금 내역 목록 조회](/api-reference/v1/transactions/list) |
| `GET` | `/v1/transactions/{id}` | [입금 단건 조회](/api-reference/v1/transactions/get) |
| `POST` | `/v1/cash-receipts` | [현금영수증 발급](/api-reference/v1/cash-receipts/issue) |
| `GET` | `/v1/cash-receipts` | [현금영수증 목록 조회](/api-reference/v1/cash-receipts/list) |
| `GET` | `/v1/cash-receipts/{id}` | [현금영수증 단건 조회](/api-reference/v1/cash-receipts/get) |
| `POST` | `/v1/cash-receipts/{id}/revoke` | [현금영수증 발급 취소](/api-reference/v1/cash-receipts/revoke) |
