Source: https://cryptanio.com/ko/docs.html
Contract / release / updated: v1 / 1.0.0 62 / 2026-10-10

# API 문서

Cryptanio를 애플리케이션에 연결하세요.

계약 v1 / 1.0.0 62 / 2026-10-10

[Markdown](https://cryptanio.com/ko/docs.md) [OpenAPI](https://cryptanio.com/openapi.json) [AI 에이전트 안내](https://cryptanio.com/docs/llms.txt)

## 시작하기

API는 서버용입니다. 먼저 Account에서 프로젝트, 서비스 및 주소 풀을 설정하세요. API는 그 범위 안에서 키에 허용된 작업을 수행합니다.

- 계정을 확인하고 [내 서비스](https://account.cryptanio.com/finance/services)에서 필요한 서비스를 활성화하세요.
- 최소 권한으로 [API 키](https://account.cryptanio.com/api-keys)를 만들고 서버에 보관하세요.
- 작업을 선택하세요: [주소 모니터링](https://cryptanio.com/ko/docs.html#quickstart), [청구서 생성](https://cryptanio.com/ko/docs.html#invoices) 또는 [이벤트 수신](https://cryptanio.com/ko/docs.html#webhooks).

예제는 실제 데이터에서 실제 작업을 수행합니다. 객체 생성이나 삭제는 서비스 할당량을 사용할 수 있습니다. 예제에는 키를 내장하지 않으며 환경 변수에서 읽습니다.

## 에이전트 지침

에이전트 채팅이나 프로젝트 규칙에 이 지침을 붙여 넣으세요. 에이전트가 서비스, 가격 및 참조 문서를 파악한 뒤 REST API 또는 SDK로 애플리케이션 통합을 작성하는 데 도움이 됩니다. 에이전트는 사용자의 언어로 답할 수 있습니다.



### Agent instructions

```text
Learn how to integrate a server-side application with Cryptanio's public REST API before proposing or writing integration code.

Start here:
- Website: https://cryptanio.com/
- Documentation index: https://cryptanio.com/llms.txt
- Product, services and pricing: https://cryptanio.com/cryptanio.md
- API integration index: https://cryptanio.com/docs/llms.txt
- API guide: https://cryptanio.com/docs.md
- Exact API v1 contract: https://cryptanio.com/openapi.json
- FAQ: https://cryptanio.com/faq/index.md
- SDK setup and examples: https://cryptanio.com/docs.html#sdk

Read the relevant method reference before using it. Use the public API at https://api.cryptanio.com/v1. Do not assume a public sandbox exists.
Use HTTP requests or the documented SDK for the application's language. Match request fields, response schemas and errors to OpenAPI; do not invent endpoints or SDK methods.
Check the actual enabled networks, service, project and API key permissions. Product documentation is not evidence of a live payment, balance or deployment.
Keep API keys in the user's server environment or secret store. Never ask the user to paste a key into chat or put it in a URL, browser bundle or log.
Default to reading. Create, change or delete objects only when the user authorizes the specific task. Explain quota or billing consequences before a write.
Follow the documented confirmation, webhook verification, pagination and retry rules. Reuse an invoice's original Idempotency-Key and request when retrying.
Show the returned chain, asset, address, amount and expiry. Preserve payment_route.kind and its exact string value: destination_tag, memo, comment or reference is mandatory when present. The merchant order reference is a separate field.
For a payment QR use the returned payment_uri when present. A Solana Pay reference is not a wallet memo; do not rebuild its URI or replace it with a plain address. Offer the exact returned checkout_url only to the payer; never construct a checkout URL from an invoice ID. Neither a checkout redirect nor a QR scan confirms payment.
Distinguish pending, confirmed, rolled-back and simulated data. If a fact is unavailable, say so instead of guessing.
Answer the user's questions in their language. If a necessary product decision is not documented, ask instead of inventing it.
```



이 블록에는 키나 계정 데이터가 없습니다. 통합에 필요한 최소 권한의 API 키를 사용하고 애플리케이션 서버에 보관하세요. 에이전트 채팅에 키를 공유하지 마세요.

## API 참조

### Monitoring 감시

[POST /v1/watches Monitoring 모니터링 생성](https://cryptanio.com/ko/docs/reference/create-watch/) [GET /v1/watches Monitoring 모니터링 목록](https://cryptanio.com/ko/docs/reference/list-watches/) [GET /v1/watches/{id} Monitoring 모니터링 조회](https://cryptanio.com/ko/docs/reference/get-watch/) [DELETE /v1/watches/{id} Monitoring 모니터링 삭제](https://cryptanio.com/ko/docs/reference/delete-watch/)

### 이벤트

[GET /v1/events 이벤트 목록](https://cryptanio.com/ko/docs/reference/list-events/)

### 결제

[GET /v1/payments Payments 이벤트 목록](https://cryptanio.com/ko/docs/reference/list-payments/)

### 청구서

[POST /v1/invoices 청구서 생성](https://cryptanio.com/ko/docs/reference/create-invoice/) [GET /v1/invoices 청구서 목록](https://cryptanio.com/ko/docs/reference/list-invoices/) [GET /v1/invoices/{id} 청구서 조회](https://cryptanio.com/ko/docs/reference/get-invoice/) [POST /v1/invoices/{id}/cancel 청구서 취소](https://cryptanio.com/ko/docs/reference/cancel-invoice/)

### Webhook

[POST /v1/endpoints 웹훅 엔드포인트 생성](https://cryptanio.com/ko/docs/reference/create-endpoint/) [GET /v1/endpoints 웹훅 엔드포인트 목록](https://cryptanio.com/ko/docs/reference/list-endpoints/) [DELETE /v1/endpoints/{id} 웹훅 엔드포인트 삭제](https://cryptanio.com/ko/docs/reference/delete-endpoint/)

### SSE

[GET /v1/stream 이벤트 스트리밍](https://cryptanio.com/ko/docs/reference/stream-events/)

### API 상태

[GET /v1/health API 상태 조회](https://cryptanio.com/ko/docs/reference/get-health/)

### 네트워크

[GET /v1/chains 네트워크 목록](https://cryptanio.com/ko/docs/reference/list-chains/)

## 인증

서버에서 `Authorization: Bearer <API_KEY>`을 보내세요. [Account → API 키](https://account.cryptanio.com/api-keys)에서 실제 데이터용 키를 생성하고 필요한 프로젝트와 작업으로 제한하세요. 현재 공개 샌드박스는 제공하지 않습니다.

키에는 읽기 전용, 특정 작업, 프로젝트나 서비스 하나, 특정 IP/CIDR 범위의 접근을 허용할 수 있습니다. 만료, 일시 중지와 취소도 접근에 영향을 줍니다. 키를 교체하려면 적절한 제한으로 새 키를 만들고 연동을 전환한 뒤 기존 키를 취소하세요. 키를 브라우저 코드, 모바일 앱이나 URL에 넣지 마세요.

| 범위 | 목적 |
| --- | --- |
| `watches` | Monitoring |
| `events` | 이벤트 및 결제 |
| `invoices` | Payments 청구서 |
| `webhooks` | 웹훅 엔드포인트 |
| `stream` | SSE |

제한은 서로 대체하지 않고 함께 적용됩니다. `401`은 키가 수락되지 않았음을 뜻합니다. `403`은 권한 부족이나 허용되지 않은 IP를 뜻할 수 있습니다. 접근 불가능한 리소스는 `404`를 반환할 수 있습니다. [오류 및 재시도](https://cryptanio.com/ko/docs.html#errors)를 확인하세요.

## SDK: 바로 사용할 수 있는 클라이언트

사용 언어의 파일을 다운로드해 애플리케이션 서버에 저장하고 생성자에 키를 전달하세요. 각 클라이언트는 이 참조 문서의 16개 메서드를 모두 지원합니다. 설치할 npm, PyPI, Composer 또는 crates.io 패키지는 없습니다. 다운로드한 파일 자체가 SDK입니다.

| 언어 | 요구 사항 | 파일 |
| --- | --- | --- |
| Node.js | Node.js 18+ | [cryptanio.mjs](https://cryptanio.com/files/sdk/node/cryptanio.mjs) |
| Python | Python 3.10+ | [cryptanio.py](https://cryptanio.com/files/sdk/python/cryptanio.py) |
| PHP | PHP 8.1+ / cURL | [Cryptanio.php](https://cryptanio.com/files/sdk/php/Cryptanio.php) |
| Go | Go 1.22+ | [cryptanio.go](https://cryptanio.com/files/sdk/go/cryptanio.go) |
| Rust | Rust 2021 / reqwest 0.12 / serde_json 1 | [cryptanio.rs](https://cryptanio.com/files/sdk/rust/cryptanio.rs) |

파일은 릴리스 `1.0.0 62`을 공유합니다. [매니페스트](https://cryptanio.com/files/sdk/manifest.json)에는 버전, 계약 및 각 파일의 SHA-256가 포함됩니다. 먼저 환경에 `CRYPTANIO_API_KEY`과 `CRYPTANIO_PROJECT_ID`를 설정하세요. 예시는 이벤트만 읽지만 실제 계정 데이터에 접근합니다.

### Node.js

예시를 cryptanio.mjs 옆에 .mjs 파일로 저장하세요.

 

### Node.js

```javascript
import { Cryptanio } from './cryptanio.mjs';
const key = process.env.CRYPTANIO_API_KEY;
const project = process.env.CRYPTANIO_PROJECT_ID;
if (!key || !project) throw new Error('Set API key and project ID');
const api = new Cryptanio(key);
const page = await api.listEvents({ project, limit: 25 });
console.log(page);
```



기존 인보이스의 결제 안내를 읽으려면 `CRYPTANIO_RESOURCE_ID`에 해당 ID를 설정하세요. 다음 예제는 필수 `payment_route`, QR용으로 반환된 `payment_uri`, 정확한 `checkout_url`을 보존합니다. 결과는 자체 인터페이스를 통해 결제자에게 전달하고 공개 로그에는 남기지 마세요. 금액은 문자열로 유지되며 없는 필드는 생략됩니다.



### Node.js

```javascript
import { Cryptanio } from './cryptanio.mjs';
const key = process.env.CRYPTANIO_API_KEY;
const invoiceID = process.env.CRYPTANIO_RESOURCE_ID;
if (!key || !invoiceID) throw new Error('Set API key and invoice ID');
const api = new Cryptanio(key);
const invoice = await api.getInvoice(invoiceID);
const payment = {
  chain: invoice.chain, asset: invoice.asset, address: invoice.address,
  amount: invoice.amount, expires_at: invoice.expires_at,
  payment_route: invoice.payment_route,
  payment_uri: invoice.payment_uri, checkout_url: invoice.checkout_url,
};
console.log(JSON.stringify(payment, null, 2));
```



### Python

예시를 cryptanio.py 옆에 .py 파일로 저장하세요.

 

### Python

```python
import os
from cryptanio import Cryptanio
api = Cryptanio(os.environ['CRYPTANIO_API_KEY'])
page = api.list_events({
    'project': os.environ['CRYPTANIO_PROJECT_ID'], 'limit': 25
})
print(page)
```



기존 인보이스의 결제 안내를 읽으려면 `CRYPTANIO_RESOURCE_ID`에 해당 ID를 설정하세요. 다음 예제는 필수 `payment_route`, QR용으로 반환된 `payment_uri`, 정확한 `checkout_url`을 보존합니다. 결과는 자체 인터페이스를 통해 결제자에게 전달하고 공개 로그에는 남기지 마세요. 금액은 문자열로 유지되며 없는 필드는 생략됩니다.



### Python

```python
import json
import os
from cryptanio import Cryptanio
api = Cryptanio(os.environ['CRYPTANIO_API_KEY'])
invoice = api.get_invoice(os.environ['CRYPTANIO_RESOURCE_ID'])
payment = {field: invoice[field] for field in (
    'chain', 'asset', 'address', 'amount', 'expires_at',
    'payment_route', 'payment_uri', 'checkout_url'
) if field in invoice}
print(json.dumps(payment, ensure_ascii=False))
```



### PHP

예시를 Cryptanio.php 옆에 .php 파일로 저장하고 cURL 확장을 활성화하세요.

 

### PHP

```php
<?php
require __DIR__ . '/Cryptanio.php';
$key = getenv('CRYPTANIO_API_KEY');
$project = getenv('CRYPTANIO_PROJECT_ID');
if (!$key || !$project) throw new RuntimeException('Set API key and project ID');
$api = new Cryptanio($key);
$page = $api->listEvents(['project' => $project, 'limit' => 25]);
echo json_encode($page, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
```



기존 인보이스의 결제 안내를 읽으려면 `CRYPTANIO_RESOURCE_ID`에 해당 ID를 설정하세요. 다음 예제는 필수 `payment_route`, QR용으로 반환된 `payment_uri`, 정확한 `checkout_url`을 보존합니다. 결과는 자체 인터페이스를 통해 결제자에게 전달하고 공개 로그에는 남기지 마세요. 금액은 문자열로 유지되며 없는 필드는 생략됩니다.



### PHP

```php
<?php
require __DIR__ . '/Cryptanio.php';
$key = getenv('CRYPTANIO_API_KEY');
$invoiceID = getenv('CRYPTANIO_RESOURCE_ID');
if (!$key || !$invoiceID) throw new RuntimeException('Set API key and invoice ID');
$api = new Cryptanio($key);
$invoice = $api->getInvoice($invoiceID);
$payment = array_intersect_key($invoice, array_fill_keys([
    'chain', 'asset', 'address', 'amount', 'expires_at',
    'payment_route', 'payment_uri', 'checkout_url'
], true));
echo json_encode($payment, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
```



### Go

Go 모듈의 `cryptanio/` 디렉터리에 파일을 배치하세요. 이 예시는 `example.local/myapp` 모듈을 사용합니다. 가져오기 경로를 자신의 모듈 이름으로 바꾸세요. 외부 의존성은 필요하지 않습니다.

 

### Go

```go
package main

import (
    "context"
    "fmt"
    "os"
    "example.local/myapp/cryptanio"
)

func main() {
    key, project := os.Getenv("CRYPTANIO_API_KEY"), os.Getenv("CRYPTANIO_PROJECT_ID")
    if key == "" || project == "" { panic("Set API key and project ID") }
    api, err := cryptanio.New(key, "", 0)
    if err != nil { panic(err) }
    page, err := api.ListEvents(context.Background(), map[string]any{
        "project": project, "limit": 25,
    })
    if err != nil { panic(err) }
    fmt.Println(page)
}
```



기존 인보이스의 결제 안내를 읽으려면 `CRYPTANIO_RESOURCE_ID`에 해당 ID를 설정하세요. 다음 예제는 필수 `payment_route`, QR용으로 반환된 `payment_uri`, 정확한 `checkout_url`을 보존합니다. 결과는 자체 인터페이스를 통해 결제자에게 전달하고 공개 로그에는 남기지 마세요. 금액은 문자열로 유지되며 없는 필드는 생략됩니다.



### Go

```go
package main

import (
    "context"
    "encoding/json"
    "os"
    "example.local/myapp/cryptanio"
)

func main() {
    key, invoiceID := os.Getenv("CRYPTANIO_API_KEY"), os.Getenv("CRYPTANIO_RESOURCE_ID")
    if key == "" || invoiceID == "" { panic("Set API key and invoice ID") }
    api, err := cryptanio.New(key, "", 0)
    if err != nil { panic(err) }
    invoice, err := api.GetInvoice(context.Background(), invoiceID)
    if err != nil { panic(err) }
    payment := make(map[string]any)
    for _, field := range []string{
        "chain", "asset", "address", "amount", "expires_at",
        "payment_route", "payment_uri", "checkout_url",
    } {
        if value, ok := invoice[field]; ok { payment[field] = value }
    }
    if err := json.NewEncoder(os.Stdout).Encode(payment); err != nil { panic(err) }
}
```



### Rust

파일을 `src/cryptanio.rs`으로, 예시를 `src/main.rs`로 저장하세요. `Cargo.toml`에 아래 의존성을 추가하세요. 동기식 차단 클라이언트이므로 비동기 작업자에서 직접 실행하지 마세요.



### Cargo.toml

```toml
[dependencies]
reqwest = { version = "=0.12.24", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde_json = "=1.0.145"
```



 

### Rust

```rust
mod cryptanio;
use cryptanio::Cryptanio;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api = Cryptanio::new(&std::env::var("CRYPTANIO_API_KEY")?)?;
    let project = std::env::var("CRYPTANIO_PROJECT_ID")?;
    let page = api.list_events(&[("project", &project), ("limit", "25")])?;
    println!("{page}");
    Ok(())
}
```



기존 인보이스의 결제 안내를 읽으려면 `CRYPTANIO_RESOURCE_ID`에 해당 ID를 설정하세요. 다음 예제는 필수 `payment_route`, QR용으로 반환된 `payment_uri`, 정확한 `checkout_url`을 보존합니다. 결과는 자체 인터페이스를 통해 결제자에게 전달하고 공개 로그에는 남기지 마세요. 금액은 문자열로 유지되며 없는 필드는 생략됩니다.



### Rust

```rust
mod cryptanio;
use cryptanio::Cryptanio;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api = Cryptanio::new(&std::env::var("CRYPTANIO_API_KEY")?)?;
    let invoice = api.get_invoice(&std::env::var("CRYPTANIO_RESOURCE_ID")?)?;
    let mut payment = serde_json::Map::new();
    for field in ["chain", "asset", "address", "amount", "expires_at",
                  "payment_route", "payment_uri", "checkout_url"] {
        if let Some(value) = invoice.get(field) {
            payment.insert(field.to_owned(), value.clone());
        }
    }
    println!("{}", serde_json::Value::Object(payment));
    Ok(())
}
```



### 메서드 호출

Node.js와 PHP는 `listEvents`, Python과 Rust는 `list_events`, Go는 `ListEvents`를 사용합니다. 각 메서드는 [참조 문서](https://cryptanio.com/ko/docs.html#reference)의 요청 하나에 대응합니다. 필터는 객체/맵이며 Rust에서는 쌍의 목록입니다. Go 메서드는 항상 `context.Context`를 첫 번째 인수로 받습니다. 알 수 없는 필터는 요청 전송 전에 거부됩니다.

| Node.js / PHP 메서드 | HTTP |
| --- | --- |
| `createWatch` | [POST /v1/watches](https://cryptanio.com/ko/docs/reference/create-watch/) |
| `listWatches` | [GET /v1/watches](https://cryptanio.com/ko/docs/reference/list-watches/) |
| `getWatch` | [GET /v1/watches/{id}](https://cryptanio.com/ko/docs/reference/get-watch/) |
| `deleteWatch` | [DELETE /v1/watches/{id}](https://cryptanio.com/ko/docs/reference/delete-watch/) |
| `listEvents` | [GET /v1/events](https://cryptanio.com/ko/docs/reference/list-events/) |
| `listPayments` | [GET /v1/payments](https://cryptanio.com/ko/docs/reference/list-payments/) |
| `createInvoice` | [POST /v1/invoices](https://cryptanio.com/ko/docs/reference/create-invoice/) |
| `listInvoices` | [GET /v1/invoices](https://cryptanio.com/ko/docs/reference/list-invoices/) |
| `getInvoice` | [GET /v1/invoices/{id}](https://cryptanio.com/ko/docs/reference/get-invoice/) |
| `cancelInvoice` | [POST /v1/invoices/{id}/cancel](https://cryptanio.com/ko/docs/reference/cancel-invoice/) |
| `createEndpoint` | [POST /v1/endpoints](https://cryptanio.com/ko/docs/reference/create-endpoint/) |
| `listEndpoints` | [GET /v1/endpoints](https://cryptanio.com/ko/docs/reference/list-endpoints/) |
| `deleteEndpoint` | [DELETE /v1/endpoints/{id}](https://cryptanio.com/ko/docs/reference/delete-endpoint/) |
| `streamEvents` | [GET /v1/stream](https://cryptanio.com/ko/docs/reference/stream-events/) |
| `getHealth` | [GET /v1/health](https://cryptanio.com/ko/docs/reference/get-health/) |
| `listChains` | [GET /v1/chains](https://cryptanio.com/ko/docs/reference/list-chains/) |

모니터링과 웹훅 생성에는 요청 본문이 필요합니다. 단일 리소스 메서드는 문자열 ID를 받습니다. `createInvoice(body, idempotencyKey)`에는 ASCII 문자, 숫자, 하이픈 또는 밑줄로 된 16–80자 키가 필요합니다. 재시도 시 같은 키와 본문을 사용하세요. SDK는 새 키를 생성하거나 자동 재시도하지 않습니다. 각 메서드에서 본문, 오류 및 권한을 확인하세요.

### 응답, 오류 및 스트리밍

JSON 응답은 필드명을 유지하며 ID, 금액 및 커서는 문자열입니다. 페이지 조회는 각 메서드 문서에 따라 수동으로 수행합니다. 오류는 HTTP 상태, code, message 및 `Retry-After`을 제공합니다. HTTP 응답이 없으면 상태는 0입니다(Rust는 `None` 사용). 리디렉션은 따르지 않습니다. 일반 JSON 응답은 8 MiB로 제한됩니다. 기본 시간 초과는 30초이며 최대 300초로 설정할 수 있습니다. Python에서는 네트워크 작업에 적용되고 다른 클라이언트에서는 스트리밍을 포함한 전체 요청에 적용됩니다. 시간 초과가 쓰기 미실행을 증명하지는 않습니다.

| 언어 / 오류 | 필드 |
| --- | --- |
| Node.js / Python: ApiError | `status, code, message, retry_after` |
| PHP: CryptanioError | `status, apiCode, getMessage(), retryAfter` |
| Go: *APIError | `Status, Code, Message, RetryAfter` |
| Rust: ApiError | `status: Option<u16>, code, message, retry_after` |

`streamEvents`은 재연결이나 이벤트 분석 없이 원시 SSE 바이트를 제공합니다. Node.js `ReadableStream`, 닫을 수 있는 Python HTTP 응답, Go `io.ReadCloser` 또는 Rust `blocking::Response`입니다. 필요 없으면 스트림을 해제하세요. PHP는 청크를 콜백에 전달하며 `false`를 반환하면 중단됩니다. 스트리밍은 웹훅을 대체하지 않고 내역 재생을 보장하지 않습니다. [SSE 규칙](https://cryptanio.com/ko/docs.html#stream)을 확인하세요.

## 첫 모니터링

Account에서 활성 Monitoring 서비스가 있는 프로젝트와 watches 및 events 권한이 있는 키를 만드세요. 필요한 경우 계정 인증을 완료하세요. 설정에서 프로젝트 ID를 복사하고 실제로 모니터링하려는 주소만 추가하세요.

서버 환경에 `CRYPTANIO_API_KEY`과 `CRYPTANIO_PROJECT_ID`을 설정하세요. 예시 주소를 본인의 주소로 바꾸세요. 실제 모니터링을 연결하는 요청이며 트랜잭션을 시뮬레이션하지 않습니다.



### Shell / cURL

```bash
#!/usr/bin/env bash
set -euo pipefail
: "${CRYPTANIO_API_KEY:?Set CRYPTANIO_API_KEY}"
: "${CRYPTANIO_PROJECT_ID:?Set CRYPTANIO_PROJECT_ID}"
json_string() {
  local text="$1" char code index output='"'
  for ((index=0; index<${#text}; index++)); do
    char="${text:index:1}"
    case "$char" in
      '"'|'\') output+="\\$char" ;;
      *) printf -v code '%d' "'$char"
         if ((code < 32)); then printf -v char '\\u%04x' "$code"; fi
         output+="$char" ;;
    esac
  done
  printf '%s"' "$output"
}
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
transfer_status=0
http_status="$(curl \
  --silent --show-error --max-time 30 --request POST \
  --header "Authorization: Bearer $CRYPTANIO_API_KEY" \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --data-binary '{"project":'"$(json_string "$CRYPTANIO_PROJECT_ID")"',"chain":"ethereum","address":"0x52908400098527886E0F7030069857D2E4169EE7","asset":"native","label":"Treasury"}' \
  "https://api.cryptanio.com/v1/watches" \
  --output "$response_file" --write-out '%{http_code}')" || transfer_status=$?
if [[ "$http_status" == 2?? ]]; then
  cat "$response_file"
  exit "$transfer_status"
fi
printf 'HTTP %s\n' "$http_status" >&2
cat "$response_file" >&2
exit 1
```



성공 응답에는 `id`이 포함되니 저장하세요. `GET /v1/watches/{id}`은 결제가 아니라 관찰 대상과 설정을 반환합니다. 발견된 활동은 `watch_id` 조건으로 필터링한 `GET /v1/events` 요청으로 확인하세요. 트랜잭션이 발견되기 전 목록이 비어 있는 것은 정상입니다. [응답, 오류와 다른 언어의 예제](https://cryptanio.com/ko/docs/reference/create-watch/).

## Payments 청구서

프로젝트에서 Payments를 활성화하고 Account에서 지원 주소를 풀에 추가하세요. 기본 프로젝트 선택에 의존하지 말고 project를 명시적으로 보내세요. amount는 암호자산 단위의 문자열이며 invoice_currency/invoice_amount는 주문의 견적을 별도로 설명합니다.

`CRYPTANIO_PROJECT_ID`은 Payments 프로젝트로, `CRYPTANIO_API_KEY`은 invoices 권한이 있는 키로 설정하세요. 새 주문의 `CRYPTANIO_OPERATION_ID`는 고유해야 하며 같은 작업을 재시도할 때는 재사용하세요. 청구서는 실제 데이터에 생성됩니다.



### Shell / cURL

```bash
#!/usr/bin/env bash
set -euo pipefail
: "${CRYPTANIO_API_KEY:?Set CRYPTANIO_API_KEY}"
: "${CRYPTANIO_OPERATION_ID:?Set CRYPTANIO_OPERATION_ID}"
: "${CRYPTANIO_PROJECT_ID:?Set CRYPTANIO_PROJECT_ID}"
json_string() {
  local text="$1" char code index output='"'
  for ((index=0; index<${#text}; index++)); do
    char="${text:index:1}"
    case "$char" in
      '"'|'\') output+="\\$char" ;;
      *) printf -v code '%d' "'$char"
         if ((code < 32)); then printf -v char '\\u%04x' "$code"; fi
         output+="$char" ;;
    esac
  done
  printf '%s"' "$output"
}
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
transfer_status=0
http_status="$(curl \
  --silent --show-error --max-time 30 --request POST \
  --header "Authorization: Bearer $CRYPTANIO_API_KEY" \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --header "Idempotency-Key: $CRYPTANIO_OPERATION_ID" \
  --data-binary '{"project":'"$(json_string "$CRYPTANIO_PROJECT_ID")"',"invoice_currency":"USD","invoice_amount":"12.50","reference":"order-42"}' \
  "https://api.cryptanio.com/v1/invoices" \
  --output "$response_file" --write-out '%{http_code}')" || transfer_status=$?
if [[ "$http_status" == 2?? ]]; then
  cat "$response_file"
  exit "$transfer_status"
fi
printf 'HTTP %s\n' "$http_status" >&2
cat "$response_file" >&2
exit 1
```



키는 `invoices`과 Payments를 허용해야 합니다. Monitoring 예시의 키는 다른 서비스로 제한되었을 수 있습니다. `Idempotency-Key`은 16-80자로, ASCII 문자, 숫자, `-` 및 `_`으로 구성됩니다. 같은 키로 동일한 정규화 요청을 반복하면 `201`와 기존 청구서를 반환하고, 내용이 바뀌면 `409 conflict`를 반환합니다. 작업 키는 계정 전체에서 적용됩니다. `reference`은 대체 수단이 아닙니다.

구매자에게 반환된 네트워크, 자산, 주소, `amount`, 만료 시간을 표시하세요. `payment_route`이 있으면 해당 `kind`와 정확한 문자열 `value`도 전달하세요. destination_tag, memo, comment 또는 reference는 이 인보이스에 결제를 연결하는 데 필수입니다. 주문의 `reference` 필드는 별개입니다.

반환된 `payment_uri`이 있으면 결제 QR에 그대로 사용하세요. Solana Pay reference는 지갑 memo가 아닙니다. URI를 단순 주소로 바꾸거나 다시 조립하지 마세요. 지갑이 필수 식별자를 포함할 수 없으면 결제자에게 `checkout_url` 또는 호환 지갑을 안내하세요.

`checkout_url`은 새로 발행된 인보이스의 호스팅 결제 페이지입니다. 반환된 정확한 URL만 결제자에게 공유하고 ID로 만들지 마세요. 링크에서 결제 상세와 상태를 볼 수 있으므로 공개 로그에 남기지 마세요. 필드가 없으면 전체 결제 안내를 표시하세요. 브라우저 리디렉션은 결제를 확인하지 않습니다. API 또는 검증된 webhook을 확인하세요.

주문을 구분하기 위해 `asked`이 amount와 다를 수 있습니다. `amount_raw`을 임의 정밀도 정수로 사용하여 대조하세요. 기본 TTL은 60분이며 최대 10080분입니다. 인보이스 목록은 `before`를 받지만 next_before를 반환하지 않습니다. 마지막 항목의 ID로 계속 조회하고 빈 페이지에서 중단하세요.

[POST /v1/invoices 청구서 생성](https://cryptanio.com/ko/docs/reference/create-invoice/) [GET /v1/invoices 청구서 목록](https://cryptanio.com/ko/docs/reference/list-invoices/) [GET /v1/invoices/{id} 청구서 조회](https://cryptanio.com/ko/docs/reference/get-invoice/) [POST /v1/invoices/{id}/cancel 청구서 취소](https://cryptanio.com/ko/docs/reference/cancel-invoice/)

### `Customer`

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `id` | `string` | 예 | 귀하의 시스템에서 사용하는 고객 식별자입니다.; maxLength: `120` |

### `Product`

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `id` | `string` | 아니요 | 사용자 시스템의 제품 식별자입니다.; maxLength: `120` |
| `name` | `string` | 아니요 | 제품 이름입니다.; maxLength: `200` |
| `quantity` | `integer` | 아니요 | 수량입니다.; minimum: `1`; maximum: `1000000` |

### `Payer`

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `comment` | `string` | 예 | 결제자 의견입니다.; maxLength: `500` |

### `InvoiceMetadata`

최대 20쌍, JSON 8192바이트까지 허용됩니다. 비밀 값을 포함하지 마세요.

`object` 최대 20쌍, JSON 8192바이트까지 허용됩니다. 비밀 값을 포함하지 마세요.

## Monitoring 감시

관찰 대상은 Monitoring 프로젝트 하나에 속합니다. 같은 주소를 여러 프로젝트에서 독립적인 방향과 자산 규칙으로 사용할 수 있습니다. 주소록 목록과 Address Sources는 Account에서 연결하세요. 이 REST API는 해당 관리 명령을 제공하지 않습니다.

목록은 ID 내림차순입니다. `next_before`이 있으면 다음 요청에서 `before`로 전달하고 프로젝트와 필터를 유지하세요. `limit`의 기본값은 50, 최대값은 200입니다. ID는 문자열로 저장하세요. 제거된 모니터링은 활성 목록에서 빠지지만 다른 프로젝트나 주소록의 주소를 삭제하지는 않습니다.

[POST /v1/watches Monitoring 모니터링 생성](https://cryptanio.com/ko/docs/reference/create-watch/) [GET /v1/watches Monitoring 모니터링 목록](https://cryptanio.com/ko/docs/reference/list-watches/) [GET /v1/watches/{id} Monitoring 모니터링 조회](https://cryptanio.com/ko/docs/reference/get-watch/) [DELETE /v1/watches/{id} Monitoring 모니터링 삭제](https://cryptanio.com/ko/docs/reference/delete-watch/)

## 이벤트 및 기록

`/v1/events`은 서비스 전체에서 키에 허용된 이벤트를 반환하고 `/v1/payments`은 Payments로 한정됩니다. project, address, chain, type, watch_id 및 tx_id로 필터링하세요. `before`는 날짜가 아닌 ID 커서입니다. from/to 필터는 없습니다. `next_before`으로 다음 페이지를 조회합니다.

감지된 이벤트 기록은 최대 30일 동안 조회할 수 있습니다. 모든 전달 메시지의 보관소는 아니며 notice와 webhook.test 메시지는 이 기록에서 복구가 보장되지 않습니다. REST Event는 웹훅과 별도의 스키마를 사용합니다. v1 내에서 필드가 추가될 수 있습니다.

[GET /v1/events 이벤트 목록](https://cryptanio.com/ko/docs/reference/list-events/) [GET /v1/payments Payments 이벤트 목록](https://cryptanio.com/ko/docs/reference/list-payments/)

### `Event`

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `event_id` | `string` | 예 | 불투명 식별자입니다. 문자열로 유지하세요.; minLength: `1` |
| `event_type` | `string` | 예 | 이벤트 유형입니다. 새로운 유형이 추가될 수 있습니다. |
| `chain` | `string` | 예 | 네트워크 코드입니다. |
| `service` | `string` | 아니요 | 알려진 경우 서비스입니다. |
| `watch_id` | `string` | 아니요 | 모니터링에 연결된 경우 프로젝트 모니터링 ID입니다.; minLength: `1` |
| `occurred_at` | `string` | 예 | RFC 3339 형식의 UTC 날짜 및 시간입니다.; `date-time` |
| `payment` | `object \| null` | 예 | 결제 문서이며, 결제 외 이벤트는 null입니다. GET /events는 contract/notice 본문을 반환하지 않습니다. |
| `sandbox` | `boolean` | 아니요 | 테스트 이벤트입니다. 생략하면 false입니다. |

### `EventPayment`

REST/SSE 결제 문서입니다. 필드는 이벤트와 네트워크에 따라 다르며, 정규화된 웹훅 v1 형식이 아닙니다.

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `tx_id` | `string` | 아니요 | 트랜잭션 식별자입니다. |
| `index` | `integer` | 아니요 | 트랜잭션 내 전송 인덱스입니다.; minimum: `0` |
| `height` | `integer` | 아니요 | 확인된 경우 블록 높이입니다.; minimum: `0` |
| `address` | `string` | 아니요 | 관찰 대상 주소입니다. |
| `asset` | `string` | 아니요 | REST/SSE 자산: 웹훅 자산 객체가 아닌 문자열 식별자입니다. |
| `amount_raw` | `string` | 아니요 | 최소 단위의 정수 금액을 문자열로 표현합니다.; pattern: `^[0-9]+$` |
| `amount` | `string` | 아니요 | 정확한 소수 금액 문자열입니다. 부동 소수점을 사용하지 마세요.; pattern: `^[0-9]+(?:\.[0-9]+)?$` |
| `decimals` | `integer` | 아니요 | 소수 자릿수입니다.; minimum: `0` |
| `confirmations` | `integer` | 아니요 | 확인 횟수입니다.; minimum: `0` |
| `revision` | `integer` | 아니요 | 전송 상태 리비전입니다.; minimum: `0` |
| `direction` | `string` | 아니요 | 관찰 대상 주소를 기준으로 한 방향입니다.; `in`, `out` |
| `simulated` | `boolean` | 아니요 | 시뮬레이션된 이벤트입니다. |
| `late` | `boolean` | 아니요 | 뒤늦게 감지된 이체입니다. |
| `block_hash` | `string` | 아니요 | 블록 해시입니다. |
| `match_key` | `string` | 아니요 | 네트워크에서 사용하는 경우의 매칭 식별자입니다. |
| `meta` | `object` | 아니요 |  |
| `invoice` | `object` | 아니요 |  |
| `customer` | `object` | 아니요 |  |
| `product` | `object` | 아니요 |  |
| `payer` | `object` | 아니요 |  |
| `metadata` | `object` | 아니요 |  |

## 웹훅 이벤트

전달은 최소 한 번 방식이므로 이벤트가 여러 번 도착할 수 있습니다. 서명 확인 후 연동 내에서 `event_id`을 기준으로 중복을 제거하세요. 처리 기록과 비즈니스 변경을 하나의 데이터베이스 트랜잭션에 저장하세요. tx_id/address/asset 키는 서로 다른 상태 변화, 방향 및 프로젝트를 합치므로 event_id를 대신할 수 없습니다.

서명된 본문에서 `event_type`과 `event_id`을 읽으세요. X-Event-Type/X-Event-Id 헤더는 HMAC에 포함되지 않으므로 사용할 경우 본문과 비교하세요. 결제 갱신 시 project_id, direction 및 payment.revision을 고려하세요. 전송 순서를 가정하지 마세요. 알 수 없는 유형은 안전하게 무시하되 진단을 위해 보관하세요.

| 계열 | 본문 |
| --- | --- |
| payment.pending / confirmed / rolled_back | 입금 |
| debit.pending / confirmed / rolled_back | 출금 결제 |
| contract.approval / owner_added / owner_removed / threshold_changed / permissions_changed | contract |
| asset.unexpected | contract. 해당 자산은 수락된 결제로 적립되지 않습니다 |
| account.notice / account.notification / treasury.alarm / subscription.notice | notice.subject, notice.text |
| webhook.test | 결제 없는 제어 이벤트 |

`schema_version: v1`에는 이벤트 유형에 따라 payment, contract 또는 notice가 포함되며 webhook.test에는 없습니다. ID와 금액은 문자열입니다. amount_raw는 최소 단위, amount는 십진수 표현입니다. 네트워크와 자산은 chains로 확인하세요. 전체 객체 스키마는 [OpenAPI](https://cryptanio.com/openapi.json)에 있습니다.

| 헤더 | 의미 |
| --- | --- |
| X-Timestamp | 서명 시점의 Unix 초 |
| X-Signature | hex HMAC-SHA256(timestamp + '.' + raw body) |
| X-Event-Id / X-Event-Type | 서명된 본문과 비교하세요. |
| X-Attempt | 1부터 시작하는 시도 번호 |

이벤트를 영구적으로 수락한 뒤 `2xx`을 반환하세요. 시도 제한 시간은 10초입니다. 네트워크 실패, 408, 425, 429 및 5xx는 최대 24시간 재시도하며, 대기 시간은 2초부터 15분까지 무작위 편차를 적용합니다. Retry-After를 준수합니다. 리디렉션은 따르지 않습니다. 다른 3xx/4xx는 전송을 종료하고 410은 대상과 대기열을 폐기합니다.



### Webhook shape example (illustrative)

```json
{
  "schema_version": "v1",
  "event_id": "2300",
  "event_type": "payment.confirmed",
  "occurred_at": "2026-10-06T10:00:00Z",
  "project_id": "1002",
  "service": "monitoring",
  "mode": "test",
  "sandbox": true,
  "payment": {
    "chain": "ethereum",
    "direction": "in",
    "asset": {
      "symbol": "ETH"
    },
    "address": "0x52908400098527886E0F7030069857D2E4169EE7",
    "tx_id": "simulated_2300",
    "index": 0,
    "amount": "1.000000000000000000",
    "amount_raw": "1000000000000000000",
    "decimals": 18,
    "confirmations": 0,
    "revision": 1,
    "simulated": true
  }
}
```



### `Webhook`

공개 서명 웹훅 v1 본문입니다. payment.* 및 debit.*에는 payment, contract.* 및 asset.unexpected에는 contract가 포함됩니다. webhook.test에는 업무 본문이 없으며 account.notice, account.notification, treasury.alarm 및 subscription.notice에는 notice가 포함됩니다. event_id로 재전송을 중복 제거하세요.

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `schema_version` | `string` | 예 | 웹훅 본문 버전입니다.; `v1` |
| `event_id` | `string` | 예 | 이벤트 ID: 전달 재시도 시 변경되지 않습니다. 상태별로 event_id 값이 다를 수 있습니다.; minLength: `1` |
| `event_type` | `string` | 예 | 서명된 본문의 유형입니다. 서명을 검증한 뒤 사용하세요. 새 유형이 추가될 수 있습니다. |
| `occurred_at` | `string` | 예 | RFC 3339 형식의 UTC 날짜 및 시간입니다.; `date-time` |
| `project_id` | `string` | 예 | 불투명 식별자입니다. 문자열로 유지하세요.; minLength: `1` |
| `service` | `string` | 아니요 | 해당하는 경우 프로젝트 서비스입니다. |
| `mode` | `string` | 예 | 이벤트 모드입니다.; `live`, `test` |
| `sandbox` | `boolean` | 예 | 명시적 모의 실행 플래그입니다. |
| `payment` | `object` | 아니요 |  |
| `contract` | `object` | 아니요 |  |
| `invoice` | `object` | 아니요 |  |
| `customer` | `object` | 아니요 |  |
| `product` | `object` | 아니요 |  |
| `payer` | `object` | 아니요 |  |
| `metadata` | `object` | 아니요 |  |
| `notice` | `object` | 아니요 |  |

### `WebhookPayment`

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `chain` | `string` | 예 | 네트워크 코드입니다. |
| `direction` | `string` | 예 | 입금: in; 출금: out.; `in`, `out` |
| `asset` | `object` | 예 |  |
| `address` | `string` | 예 | 관찰 대상 주소입니다. |
| `tx_id` | `string` | 예 | 트랜잭션 ID입니다. |
| `index` | `integer` | 예 | 전송 인덱스입니다. 모든 네트워크에서 0라고 가정하지 마세요.; minimum: `0` |
| `height` | `integer` | 아니요 | 알려진 경우 블록 높이입니다.; minimum: `0` |
| `block_hash` | `string` | 아니요 | 알려진 경우 블록 해시입니다. |
| `match_key` | `string` | 아니요 | 해당되는 경우 네트워크 매칭 식별자입니다. |
| `amount` | `string` | 예 | 정확한 소수 금액 문자열입니다. 부동 소수점을 사용하지 마세요.; pattern: `^[0-9]+(?:\.[0-9]+)?$` |
| `amount_raw` | `string` | 예 | 최소 단위의 정수 금액을 문자열로 표현합니다.; pattern: `^[0-9]+$` |
| `decimals` | `integer` | 예 | 소수 자릿수입니다.; minimum: `0` |
| `confirmations` | `integer` | 예 | 확인 횟수입니다.; minimum: `0` |
| `revision` | `integer` | 예 | 전송 상태 리비전입니다. 트랜잭션 식별자만으로 다른 event_id 값을 합치지 마세요.; minimum: `0` |
| `late` | `boolean` | 아니요 | 뒤늦게 감지된 이체이며 청구서 상태 late와는 다릅니다. |
| `simulated` | `boolean` | 아니요 | 시뮬레이션된 전송입니다. |
| `valuation` | `object` | 아니요 |  |

### `WebhookAsset`

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `symbol` | `string` | 아니요 | 알려진 경우 기호입니다. 기호만으로 토큰을 식별할 수 없습니다. |
| `standard` | `string` | 아니요 | 알려진 경우 토큰 표준입니다. |
| `contract` | `string` | 아니요 | 해당하는 경우 네트워크 내 토큰 계약입니다. |
| `identifier` | `string` | 아니요 | 네트워크별 다른 토큰 식별자입니다. |

### `WebhookContract`

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `chain` | `string` | 예 | 네트워크 코드입니다. |
| `address` | `string` | 예 | 관찰 대상 주소 또는 계약입니다. |
| `tx_id` | `string` | 예 | 트랜잭션 ID입니다. |
| `index` | `integer` | 예 | 이벤트 인덱스입니다.; minimum: `0` |
| `height` | `integer` | 아니요 | 블록 높이입니다.; minimum: `0` |
| `block_hash` | `string` | 아니요 | 블록 해시입니다. |
| `kind` | `string` | 아니요 | 변경 유형입니다. |
| `asset` | `string` | 아니요 | 알려진 경우 영향을 받은 자산 식별자입니다. |
| `detail` | `object` | 아니요 |  |
| `simulated` | `boolean` | 아니요 | 시뮬레이션된 이벤트입니다. |

### `WebhookInvoice`

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `id` | `string` | 예 | 불투명 식별자입니다. 문자열로 유지하세요.; minLength: `1` |
| `reference` | `string` | 아니요 | 주문 참조입니다. |
| `status` | `string` | 아니요 | pending은 결제 대기, seen은 감지됨, partial은 일부 결제, paid는 정산 완료입니다. underpaid/overpaid/late는 판단이 필요하며 expired는 기한 만료, canceled는 취소입니다.; `pending`, `seen`, `paid`, `underpaid`, `partial`, `overpaid`, `expired`, `late`, `canceled` |
| `currency` | `string` | 아니요 | 주문 견적 통화입니다. |
| `amount` | `string` | 아니요 | 정확한 소수 금액 문자열입니다. 부동 소수점을 사용하지 마세요.; pattern: `^[0-9]+(?:\.[0-9]+)?$` |

### `Notice`

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `subject` | `string` | 예 | 고지 제목입니다. |
| `text` | `string` | 예 | 고지 본문입니다. |

### `Valuation`

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `currency` | `string` | 예 | USD 등 평가 통화입니다. |
| `amount` | `string` | 예 | 정확한 소수 금액 문자열입니다. 부동 소수점을 사용하지 마세요.; pattern: `^[0-9]+(?:\.[0-9]+)?$` |
| `rate` | `string` | 예 | 소수 문자열로 표현한 자산 환율입니다.; pattern: `^[0-9]+(?:\.[0-9]+)?$` |
| `rate_at` | `string` | 예 | RFC 3339 형식의 UTC 날짜 및 시간입니다.; `date-time` |
| `provider` | `string` | 아니요 | 알려진 경우 시세 출처 이름입니다. |

## 서명 검증

JSON 분석 전에 공백과 줄바꿈을 포함한 원본 본문 바이트로 HMAC을 검증하세요. 시간 검사는 재생 공격을 제한하지만 영구적인 event_id 중복 제거를 대체하지 않습니다. 예시는 64자 16진수 서명과 서버 시간 기준 300초 이내의 타임스탬프만 허용합니다. 해당 웹훅 대상의 비밀 값을 사용하세요.



### Node.js

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";
import { TextDecoder } from "node:util";

// rawBody is the original HTTP request Buffer, before JSON.parse or serialization.
export function verifyWebhook(secret, timestamp, rawBody, signature, nowSeconds = Math.floor(Date.now() / 1000)) {
  if (typeof secret !== "string" || secret.length === 0 || !Buffer.isBuffer(rawBody) || rawBody.length === 0 ||
      typeof timestamp !== "string" || !/^(0|[1-9][0-9]*)$/.test(timestamp) ||
      typeof signature !== "string" || !/^[0-9a-f]{64}$/i.test(signature) ||
      !Number.isSafeInteger(nowSeconds) || nowSeconds < 0) return false;

  const signedAt = Number(timestamp);
  if (!Number.isSafeInteger(signedAt) || Math.abs(nowSeconds - signedAt) > 300) return false;

  const expected = createHmac("sha256", secret).update(timestamp).update(".").update(rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(signature, "hex"));
}

// headers is Node.js request.headers (lowercase names). Optional X-Event-Id/Type
// headers are not signed separately; compare them with the verified body.
export function readSignedWebhook(secret, timestamp, rawBody, signature, headers = {}, nowSeconds) {
  if (!verifyWebhook(secret, timestamp, rawBody, signature, nowSeconds) ||
      headers === null || typeof headers !== "object" || Array.isArray(headers)) return null;

  let event;
  try {
    event = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(rawBody));
  } catch {
    return null;
  }
  if (event === null || typeof event !== "object" || Array.isArray(event) || event.schema_version !== "v1" ||
      typeof event.event_id !== "string" || event.event_id.length === 0 ||
      typeof event.event_type !== "string" || event.event_type.length === 0) return null;

  for (const [header, field] of [["x-event-id", "event_id"], ["x-event-type", "event_type"]]) {
    if (headers[header] !== undefined && headers[header] !== event[field]) return null;
  }
  return event;
}
```



[Node.js 예시 다운로드](https://cryptanio.com/files/examples/webhook-node.mjs)



### Go

```go
package webhook

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"strconv"
	"time"
	"unicode/utf8"
)

// VerifyWebhook checks the original HTTP body bytes before parsing JSON.
// Optional nowSeconds is used only for deterministic tests.
func VerifyWebhook(secret, timestamp string, rawBody []byte, signature string, nowSeconds ...int64) bool {
	if secret == "" || len(rawBody) == 0 || len(signature) != 64 || len(nowSeconds) > 1 {
		return false
	}
	signedAt, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil || signedAt < 0 || strconv.FormatInt(signedAt, 10) != timestamp {
		return false
	}
	now := time.Now().Unix()
	if len(nowSeconds) == 1 {
		now = nowSeconds[0]
	}
	if now < 0 || now-signedAt < -300 || now-signedAt > 300 {
		return false
	}
	provided, err := hex.DecodeString(signature)
	if err != nil || len(provided) != sha256.Size {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(timestamp))
	mac.Write([]byte("."))
	mac.Write(rawBody)
	return hmac.Equal(mac.Sum(nil), provided)
}

// WebhookEvent contains only routing fields. Parse other fields from rawBody
// after ReadSignedWebhook succeeds; unknown fields are allowed.
type WebhookEvent struct {
	SchemaVersion string `json:"schema_version"`
	EventID       string `json:"event_id"`
	EventType     string `json:"event_type"`
}

// ReadSignedWebhook extracts event_id/type only from the verified body.
// X-Event-Id/Type are not signed separately; nonempty values must match the body.
func ReadSignedWebhook(secret, timestamp string, rawBody []byte, signature, eventIDHeader, eventTypeHeader string,
	nowSeconds ...int64) (*WebhookEvent, error) {
	if !VerifyWebhook(secret, timestamp, rawBody, signature, nowSeconds...) {
		return nil, errors.New("invalid webhook signature or timestamp")
	}
	var event WebhookEvent
	if !utf8.Valid(rawBody) || json.Unmarshal(rawBody, &event) != nil || event.SchemaVersion != "v1" ||
		event.EventID == "" || event.EventType == "" {
		return nil, errors.New("invalid webhook body")
	}
	if eventIDHeader != "" && eventIDHeader != event.EventID || eventTypeHeader != "" && eventTypeHeader != event.EventType {
		return nil, errors.New("webhook headers do not match the signed body")
	}
	return &event, nil
}
```



[Go 예시 다운로드](https://cryptanio.com/files/examples/webhook-go.go)

## 웹훅 엔드포인트

특정 프로젝트에 포트 443의 공개 HTTPS URL을 추가하세요. URL의 자격 증명, 프래그먼트, 리디렉션과 비공개 목적지는 허용되지 않습니다. 계정당 최대 20개 엔드포인트를 사용할 수 있습니다. 각각 별도 비밀 값이 있으며 최초 생성 시에만 반환되므로 서버에 저장하세요.

같은 프로젝트와 모드에서 같은 URL을 다시 생성하면 200과 비밀 값이 제외된 기존 엔드포인트를 반환합니다. 새 엔드포인트는 201를 반환합니다. DELETE는 안전한 반복 호출을 포함하여 `200`과 `id`, `deleted: true`를 반환합니다. 키에는 해당 모드와 허용된 프로젝트만 표시되며 전달은 프로젝트 설정을 따릅니다.

[POST /v1/endpoints 웹훅 엔드포인트 생성](https://cryptanio.com/ko/docs/reference/create-endpoint/) [GET /v1/endpoints 웹훅 엔드포인트 목록](https://cryptanio.com/ko/docs/reference/list-endpoints/) [DELETE /v1/endpoints/{id} 웹훅 엔드포인트 삭제](https://cryptanio.com/ko/docs/reference/delete-endpoint/)

## SSE 스트림

SSE는 서버 통합과 실시간 표시용입니다. 전달이나 누락 이벤트 재생을 보장하지 않습니다. Bearer 헤더를 사용하는 서버 측 HTTP 클라이언트를 사용하세요. 표준 브라우저 EventSource는 이 헤더를 보낼 수 없습니다. 브라우저 코드나 쿼리 문자열에 API 키를 넣지 마세요.

메시지는 event: payment와 자체 StreamEvent 구조를 사용하며 webhook v1이나 REST Event와 다릅니다. 주석은 콜론으로 시작하고 하트비트는 약 20초마다 도착합니다. retry: 5000은 재연결 힌트입니다. 연결이 끊긴 후에는 events에서 조회 가능한 블록체인 이벤트 기록을 가져오세요.

[GET /v1/stream 이벤트 스트리밍](https://cryptanio.com/ko/docs/reference/stream-events/)

### `StreamEvent`

SSE data의 JSON이며 webhook v1과 다릅니다. 결제 외 이벤트는 payment를 생략하며 contract를 포함할 수 있습니다.

| 필드 | 유형 | 필수 | 설명 |
| --- | --- | --- | --- |
| `event_id` | `string` | 예 | 불투명 식별자입니다. 문자열로 유지하세요.; minLength: `1` |
| `event_type` | `string` | 예 | 이벤트 유형입니다. 새로운 유형이 추가될 수 있습니다. |
| `chain` | `string` | 예 | 네트워크 코드입니다. |
| `service` | `string` | 아니요 | 알려진 경우 서비스입니다. |
| `watch_id` | `string` | 아니요 | 모니터링에 연결된 경우 프로젝트 모니터링 ID입니다.; minLength: `1` |
| `occurred_at` | `string` | 예 | RFC 3339 형식의 UTC 날짜 및 시간입니다.; `date-time` |
| `payment` | `object \| null` | 아니요 | 결제 문서이며, 결제 외 이벤트는 null입니다. GET /events는 contract/notice 본문을 반환하지 않습니다. |
| `sandbox` | `boolean` | 아니요 | 테스트 이벤트입니다. 생략하면 false입니다. |
| `project_id` | `string` | 아니요 | 불투명 식별자입니다. 문자열로 유지하세요.; minLength: `1` |
| `project_name` | `string` | 아니요 | 프로젝트 이름입니다. |
| `url` | `string` | 아니요 | Account 이벤트 링크입니다.; `uri` |
| `contract` | `object` | 아니요 |  |
| `mode` | `string` | 아니요 | live 또는 test입니다.; `live`, `test` |

## 결제 및 청구서 상태

블록체인 결제 상태와 주문 상태는 다릅니다. payment.pending은 충분한 확인 전의 감지, payment.confirmed는 네트워크/프로젝트의 확인 규칙 충족, payment.rolled_back은 거래 되돌림을 의미합니다. pending, 출금 또는 모의 이벤트를 근거로 주문을 이행하지 마세요.

| Invoice | 의미 |
| --- | --- |
| pending | 결제 대기 중 |
| partial | 일부 입금됨. 프로젝트의 부분 결제 정책이 적용됩니다 |
| paid | 결제 수락됨 |
| underpaid | 부족 입금은 판단이 필요합니다 |
| overpaid | 초과 입금은 적용 정책에 따른 판단이 필요합니다 |
| late | 만료 후 결제는 판단이 필요합니다 |
| expired | 결제 기한 만료 |
| canceled | 청구서가 취소되었습니다. 기록은 계속 조회할 수 있습니다. |

`needs_decision`은 underpaid, overpaid 및 late에 있습니다. Account에서 해결하세요. 체인 재구성과 재포함은 새 상태 전환 및 새 `payment.revision`을 만들 수 있습니다. confirmed는 향후 변경이 없음을 무조건 보장하지 않습니다.

## 오류 및 안전한 재시도



### application/json

```json
{"error":{"code":"forbidden","message":"this API key is read-only"}}
```



로직에는 HTTP 상태 및 error.code를, 진단에는 message를 사용하세요. 성공한 목록에는 data가 있고 단일 객체는 직접 반환됩니다. Health는 503 응답을 포함해 별도 status/version 형식을 사용합니다. 정확한 메시지 문구에 의존하지 마세요.

| HTTP | 작업 |
| --- | --- |
| 400 | JSON, 형식 및 매개변수를 수정하세요. 변경 없이 재시도하지 마세요. |
| 401 | 키, 만료, 일시 중지 및 취소 상태를 확인하세요. |
| 403 | 범위, 읽기/쓰기, 프로젝트, 서비스, IP 및 계정 인증을 확인하세요. |
| 404 | ID, 키 모드 및 접근 권한을 확인하세요. 제한에 따라 리소스가 숨겨질 수 있습니다. |
| 402 | Monitoring 감시 한도입니다. 요금제를 확인하세요. |
| 409 | 충돌, 비활성 서비스, 사용 중인 풀 또는 한도입니다. 코드를 확인하세요. |
| 429 | Retry-After를 준수하고 계정의 전체 요청 속도를 줄이세요 |
| 500 / 503 | 제한된 점진적 대기로 재시도하세요. 청구서 생성은 원래 Idempotency-Key만 사용하세요 |

시간 초과는 작업이 실행되지 않았다는 뜻이 아닙니다. 재시도 시 청구서의 작업 키를 바꾸지 마세요. 중복 방지 기능이 없는 메서드는 먼저 상태를 읽으세요. 모든 POST가 멱등적이라고 가정하면 안 됩니다.

## 네트워크 및 한도

선택한 API 호스트의 chains로 네트워크, 자산, 기능 및 확인 카탈로그를 조회하세요. 일반 카탈로그에 있다고 해서 해당 환경에서 활성화되거나 모든 작업을 지원하는 것은 아닙니다. 서비스 사용량 한도와 예산은 Account에서, 가격은 요금 페이지에서 확인하세요.

계정의 인증된 REST 한도는 유료 서비스가 없으면 초당 10회, 있으면 초당 50회입니다. 짧은 급증 허용량은 20/100입니다. 계정의 모든 키와 모드가 이 한도를 공유합니다. 초과하면 Retry-After와 함께 429을 반환합니다. 서비스 사용량 한도와 API 요청 속도 제한은 별도입니다.

[서비스 요금 및 한도](https://cryptanio.com/ko/pricing/)

[GET /v1/health API 상태 조회](https://cryptanio.com/ko/docs/reference/get-health/) [GET /v1/chains 네트워크 목록](https://cryptanio.com/ko/docs/reference/list-chains/)

## API 버전

이 참조는 `v1`(릴리스 `1.0.0 62`)을 설명하며 2026-10-10에 갱신되었습니다. 새 기능을 사용하기 전에 `GET /v1/health`로 배포 버전을 확인하세요. 문서 버전은 배포를 확인하지 않습니다.

| 공개 API | 계약 |
| --- | --- |
| `https://api.cryptanio.com` | `/v1` |

안정적인 통합에는 `/v1`을 사용하세요. 버전 없는 경로는 환경의 현재 안정 계약을 선택하며, 알 수 없는 `/vN`은 `404`를 반환합니다. `X-API-Version`은 계약을, `X-Server-Version` 및 `health.version`는 제품 릴리스를 뜻하며 다른 API 계약이 아닙니다.

[OpenAPI 3.1 다운로드](https://cryptanio.com/openapi.json)

## API 변경 사항

릴리스 `1.0.0 62`에는 Node.js, Python, PHP, Go 및 Rust용 다운로드 SDK와 [AI 에이전트용 로컬 MCP](https://cryptanio.com/ko/mcp/)가 포함됩니다. 참조, Markdown 및 원시 HTTP 예시는 별도로 제공됩니다. 공개 샌드박스는 없습니다. API 동작은 변경되지 않습니다.

v1 클라이언트는 추가 응답 필드를 허용해야 합니다. 알 수 없는 이벤트 유형을 수락해도 주문 이행이나 결제 적립을 뜻하지 않습니다. 사용 전에 선택한 환경의 버전에서 변경 기능을 제공하는지 확인하세요.
