> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# 거래 모니터링 SDK 거래 제출 - 클라이언트 측 KYT

> Didit 스코프 토큰으로 앱에서 모니터링 대상 거래를 제출하세요. 기기 인텔리전스 자동 수집, 위변조 방지 주체 바인딩, 지갑 소유권 확인 흐름을 제공합니다.

Didit SDK를 사용하면 최종 사용자의 기기에서 직접 [모니터링 대상 거래](https://docs.didit.me/ko/transaction-monitoring/transactions.md)를 제출할 수 있으며, [Travel Rule](https://docs.didit.me/ko/transaction-monitoring/travel-rule.md) 송금과 암호화폐 스크리닝도 포함됩니다.
백엔드에서 수명이 짧은 범위 지정 토큰을 발급하면, 나머지는 SDK가 처리합니다.
SDK는 기기 인텔리전스를 자동으로 첨부하며, Travel Rule 송금에 지갑 소유 증명이 필요하면 해당 절차를 즉시 자동으로 실행합니다.
필수 인증 세션은 자동으로 실행되지 않습니다. 응답으로 반환되므로, 앱에서 직접 Didit 인증 연동으로 실행해야 합니다. 아래 [필요한 사용자 작업](#필수-사용자-작업)을 참고하십시오.

## 개요

클라이언트 측 제출은 세 가지 보장을 기반으로 합니다.

* **스푸핑 방지 주체 바인딩.** 토큰은 발급 시점에 하나의 `vendor_data`(사용자 ID)에 범위가 지정됩니다. 이 토큰으로 제출되는 모든 거래의 주체 신원은 해당 바인딩에 따라 서버 측에서 강제 적용됩니다. 따라서 변조된 클라이언트는 다른 사용자를 대신해 제출할 수 없으며, 페이로드에 자격 증명을 포함할 필요도 없습니다.
* **자동 기기 인텔리전스.** SDK는 기기 핑거프린트를 수집해 제출과 함께 전송하며, Didit은 IP 주소와 사용자 에이전트를 서버 측에서 도출합니다. 결합된 기기 컨텍스트는 추가 라이브러리, 별도의 기기 토큰, 개발자 측 구현 없이 거래의 주체에 첨부됩니다. 이 정보는 기기 기반 [규칙](https://docs.didit.me/ko/transaction-monitoring/rules.md)과 세션 및 거래 전반에 걸친 기기 기준 연관 분석을 지원합니다.
* **자동 실행되는 지갑 소유 증명.** 생성된 거래에서 최종 사용자가 지갑 소유를 증명해야 하면, 응답에 `wallet_ownership` 유형의 `action_required` 블록이 포함되고 SDK는 즉시 해당 절차를 엽니다. 절차가 완료되면 거래를 새로 고칩니다. 폴링 루프나 링크 전달을 직접 구현할 필요가 없습니다. 필수 `verification_session` 작업은 이와 다릅니다. 이 작업은 자동으로 실행되지 않으며, 같은 블록에 반환되어 앱이 자체 Didit 인증 연동으로 실행하도록 합니다.

> [!NOTE]
>
> 최종 사용자가 없는 경우에는 서버 측 [`POST /v3/transactions/`](https://docs.didit.me/ko/transaction-monitoring/transactions)가 여전히 적합한 선택입니다. 예를 들어 백오피스 송금, 일괄 가져오기, 자체 시스템이 보고한 이벤트가 이에 해당합니다.
> 두 경로 모두 동일한 거래 레코드를 생성하고, 동일한 규칙을 실행하며, 동일한 [webhook](https://docs.didit.me/ko/transaction-monitoring/webhooks.md)을 발송합니다.

## 흐름

**1. 거래 토큰 발급**

백엔드에서 API 키와 사용자의 `vendor_data`로 `POST /v3/transactions/sdk-token/`를 호출하고, 반환된 `sdk_token`을 앱에 전달합니다.

**2. 기기에서 제출**

앱이 SDK의 `submitTransaction`을 호출하면, SDK는 `X-Transaction-Token` 헤더와 자동 기기 보강 정보를 포함해 페이로드를 `POST /v1/transactions/`로 전송합니다.

**3. 필수 작업 처리**

`action_required.type`이 `wallet_ownership`이면 SDK가 위젯을 자동으로 실행한 다음, `GET /v1/transactions/{transaction_id}/`로 거래를 새로 고칩니다.
`verification_session`이면 SDK는 아무것도 실행하지 않습니다. `action_required`를 읽고, `session_token`(웹에서는 `url`)으로 자체 인증 절차를 시작하십시오.

## 거래 토큰 발급

백엔드에서 `POST /v3/transactions/sdk-token/`로 토큰을 발급합니다. 이때 `/v3/` 거래 엔드포인트의 나머지와 동일한 API 키 인증을 사용합니다.

```bash theme={null}
curl -X POST https://verification.didit.me/v3/transactions/sdk-token/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "vendor_data": "user-042",
    "ttl_seconds": 900,
    "max_uses": 3
  }'
```

```json theme={null}
{
  "sdk_token": "q7Zt...urlsafe",
  "expires_at": "2026-07-07T12:15:00Z"
}
```

| 필드 | 타입 | 설명 |
| - | - | - |
| `vendor_data` | string | **필수 항목입니다.** 이 토큰이 범위를 지정하는 최종 사용자를 나타내는 내부 식별자입니다. 토큰으로 제출되는 모든 거래의 주체 신원은 이 값에 따라 강제 적용됩니다. |
| `ttl_seconds` | integer | 토큰 수명(초)입니다. 기본값은 `900`(15분)이며, 최대값은 `86400`(24시간)입니다. |
| `max_uses` | integer | 선택 사항입니다. 이 토큰으로 성공할 수 있는 최대 제출 횟수입니다. TTL 내에서 무제한으로 사용하려면 생략하십시오. |

camelCase 별칭(`vendorData`, `ttlSeconds`, `maxUses`)도 나머지 `/v3/` API와 마찬가지로 허용됩니다.

> [!WARNING]
>
> **앱에 API 키를 절대 포함하지 마십시오.**
> API 키는 백엔드에 두고 토큰을 발급하는 데만 사용합니다. 기기로 전달되는 것은 수명이 짧은 `sdk_token`뿐입니다.
> 사용자 세션마다 새 토큰을 발급하고, 장기간 사용하는 토큰을 재사용하지 마십시오. 예상 제출 횟수를 알고 있다면 `max_uses`를 엄격하게 설정하십시오.

## 기기에서 제출하기

SDK는 토큰을 `X-Transaction-Token` 헤더에 담아 `POST /v1/transactions/`로 전송합니다.
전송 페이로드는 camelCase입니다. `submitTransaction` 메서드가 페이로드를 구성하므로, 자체 HTTP 클라이언트에서 엔드포인트를 직접 호출할 때만 이 형식을 직접 다루면 됩니다:

```bash theme={null}
curl -X POST https://verification.didit.me/v1/transactions/ \
  -H "X-Transaction-Token: SDK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "txnId": "wd-2026-07-07-0042",
    "type": "travelRule",
    "info": {
      "direction": "out",
      "amount": "0.25",
      "currency": "ETH",
      "currencyType": "crypto",
      "cryptoParams": { "crypto_chain": "ETH" }
    },
    "subject": { "fullName": "Jane Doe" },
    "counterparty": {
      "fullName": "Ana Diaz",
      "paymentMethod": { "type": "unhosted_wallet", "accountId": "0xBeneficiaryWallet01" }
    },
    "travelRule": {
      "status": "AWAITING_COUNTERPARTY",
      "beneficiaryData": { "wallet_address": "0xBeneficiaryWallet01", "name": "Ana Diaz" }
    },
    "includeCryptoScreening": true
  }'
```

각 최상위 필드는 [`POST /v3/transactions/`](https://docs.didit.me/ko/transaction-monitoring/transactions)의 대응 필드에 매핑됩니다:

| 전송 필드 | `/v3/` 대응 필드 | 비고 |
| - | - | - |
| `txnId` | `transaction_id` | **필수 항목입니다.** 거래의 고유 식별자입니다. |
| `txnDate` | `transaction_at` | ISO-8601 타임스탬프이며, 기본값은 현재 시각입니다. |
| `zoneId` | `time_zone` | IANA 시간대 ID이며, 예: `Europe/Madrid`. |
| `type` | `transaction_category` | **필수 항목입니다.** `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet` 및 그 밖의 카테고리입니다. |
| `info` | `transaction_details` | **필수 항목입니다.** `direction`, `amount`, `currency`(필수), 그리고 `currencyType`, `amountInDefaultCurrency`, `defaultCurrencyCode`, `paymentDetails`, `paymentTxnId`, `type`(작업 유형), `cryptoParams`. |
| `subject`, `counterparty` | `subject`, `counterparty` | 참여자: `type`(주체 유형), `externalUserId`, `fullName`, `firstName`, `lastName`, `dob`, `address`, `institutionInfo`, `device`, `paymentMethod`(`type`, `accountId`, `issuingCountry`). 카운터파티의 `paymentMethod.type`이 `unhosted_wallet`이면 대상이 자체 보관 지갑임을 선언하는 것입니다. 자세한 내용은 [아래](#자체-보관-지갑-선언하기)를 참고하십시오. |
| `props` | `custom_properties` | 사용자 정의 키-값 쌍이며, 규칙에서 `custom_values.<key>`로 사용할 수 있습니다. |
| `travelRule` | `travel_rule_details` | `status`, `protocol`, `required`, `obligationsCount`, `originatorData`, `beneficiaryData`, `metadata`. 이 블록을 보낼 때는 `status`가 항상 필요하며, 없으면 요청이 `400`으로 실패합니다. Travel Rule이 활성화된 경우 Didit는 전달받은 `status`를 무시하고 교환 과정을 직접 진행합니다. |
| `includeCryptoScreening` | `include_crypto_screening` | 거래별 암호화폐 스크리닝 재정의 설정입니다. |

응답은 `/v3/` 엔드포인트와 동일한 snake\_case 거래 상세 정보에 `action_required` 블록이 더해진 형태입니다:

```json theme={null}
{
  "uuid": "22222222-3333-4444-5555-666666666666",
  "txn_id": "wd-2026-07-07-0042",
  "transaction_type": "travelRule",
  "direction": "OUTBOUND",
  "status": "APPROVED",
  "amount": "0.25",
  "currency": "ETH",
  "travel_rule": {
    "uuid": "33333333-4444-5555-6666-777777777777",
    "status": "UNCONFIRMED_OWNERSHIP",
    "required": true
  },
  "action_required": {
    "type": "wallet_ownership",
    "url": "https://verify.didit.me/wallet-ownership/3s9x...urlsafe",
    "widget_session_id": "77777777-8888-9999-aaaa-bbbbbbbbbbbb",
    "expires_at": "2026-07-07T13:00:00Z"
  }
}
```

### 자체 보관 지갑 선언하기

위 예시는 출금 대상을 사용자 본인의 자체 보관 지갑으로 선언합니다. 즉 카운터파티의 `paymentMethod.type`이 `unhosted_wallet`입니다.
이 선언은 [Travel Rule 교환](https://docs.didit.me/ko/transaction-monitoring/travel-rule.md#선언된-자체-보관-지갑)의 진행 방식을 바꿉니다:

* Didit는 카운터파티 VASP 라우팅을 완전히 건너뛰고(자체 보관 지갑 뒤에는 VASP가 없습니다), 전송을 `UNCONFIRMED_OWNERSHIP` 상태로 바로 생성합니다.
* [Travel Rule 설정](https://docs.didit.me/ko/transaction-monitoring/travel-rule.md#설정)에서 `auto_wallet_verification`이 켜져 있으면(기본값), 위 응답과 똑같이 [지갑 소유권 위젯](https://docs.didit.me/ko/transaction-monitoring/wallet-ownership-widget.md) 세션이 자동으로 생성되어 `action_required`에 반환됩니다.
* SDK가 위젯을 자동으로 실행합니다. 사용자가 지갑에 대한 통제권을 증명하면 전송은 이름 대조를 수행하고, `COMPLETED`로 진행합니다. 이름이 지갑 주소록 소유자와 일치하지 않으면 `COUNTERPARTY_MISMATCHED_DATA`로 진행합니다.

선언이 없으면 Didit는 대신 [레일](https://docs.didit.me/ko/transaction-monitoring/travel-rule.md#개요)을 순서대로 시도하고, 카운터파티 VASP를 찾을 수 없는 경우에만 소유권 흐름으로 대체합니다.
출금 대상이 사용자 본인의 지갑이라는 것을 제품에서 이미 알고 있다면 언제든 지갑을 선언하십시오. 실패한 라우팅 시도를 생략하고 사용자를 증명 흐름으로 곧바로 안내할 수 있습니다.

### 주체 기준 적용

다음 세 가지는 항상 페이로드가 아니라 토큰이 결정합니다:

* 거래가 생성되는 애플리케이션.
* 주체의 신원: 페이로드의 `subject.externalUserId`는 무시되고 토큰의 `vendor_data`로 대체됩니다.
* 주체의 기기 컨텍스트(아래 참고). 서버에서 도출된 필드는 `subject.device`로 전달된 값과 충돌할 경우 우선합니다.

이 엔드포인트에서 `401`이 반환되면 토큰이 거부된 것입니다. 토큰이 없거나, 알 수 없거나, 폐기되었거나, `max_uses`를 초과했거나, 만료된 경우입니다.
만료된 토큰 응답의 오류 상세에는 항상 `expired`라는 단어가 포함됩니다. SDK는 이 단어로 `expired_token` 오류와 `invalid_token` 오류를 구분합니다.

### 거래 상세 다시 조회하기

`GET /v1/transactions/{transaction_id}/`는 동일한 `X-Transaction-Token` 인증으로 같은 상세 정보를 반환합니다.
SDK는 실행된 작업 흐름이 완료되거나 그 창이 닫힌 후 이 엔드포인트를 자동으로 호출하므로, 최종 상태가 단일 이벤트나 알림에 의존하지 않습니다.
토큰이 유효한 동안 언제든 직접 이 엔드포인트를 사용해 `status`와 `action_required`를 다시 확인할 수 있습니다. 백엔드에서는 [`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/ko/transaction-monitoring/transactions)가 같은 레코드를 반환합니다.

## 자동 기기 인텔리전스

모든 제출에는 별도의 연동 작업 없이 기기 인텔리전스가 포함됩니다:

* SDK는 개인정보를 보호하는 기기 지문(`didit-fp-v2` 스키마: 플랫폼 및 내비게이터 신호, 지속적인 기기 ID, 봇 점수, 복합 해시)을 수집하여 요청과 함께 전송합니다.
* Didit는 연결 자체로부터 서버 측에서 IP 주소, 사용자 에이전트, accept-language 값을 도출하므로, 페이로드로 위조할 수 없습니다.

병합된 결과는 주체 참여자의 기기 컨텍스트로 정규화되어, KYC 세션이 데이터를 공급하는 것과 같은 기기 그래프에 삽입 또는 업데이트됩니다.
이로써 전체 사용자 기반에 걸쳐 거래를 기기 기준으로 상관 분석할 수 있으며("기기 기준 연관"), 기기 및 네트워크 신호를 [규칙](https://docs.didit.me/ko/transaction-monitoring/rules.md)과 [IP 정보 보강](https://docs.didit.me/ko/transaction-monitoring/ip-enrichment.md)에 반영하고, [콘솔](https://docs.didit.me/ko/transaction-monitoring/console.md)의 거래 상세 정보에도 표시됩니다.

서버에서 제출하는 `/v3/` 거래는 참여자별로 명시적인 `device_context` 객체를 계속 전달할 수 있습니다. SDK 경로에서는 같은 데이터의 더 강력한 버전을 추가 작업 없이 얻을 수 있습니다.

## 필수 사용자 작업

일부 거래는 정산되기 전에 최종 사용자가 무언가를 해야 합니다. 규칙이 인증 세션을 요구할 수도 있고, Travel Rule 이체가 목적지 지갑이 사용자 본인의 지갑이라는 증명을 요구할 수도 있습니다.
두 경우 모두 같은 위치, 즉 제출 응답(그리고 이후 거래를 조회할 때마다 반환되는 응답)의 `action_required` 블록에 나타납니다.

```json theme={null}
"action_required": {
  "type": "verification_session",
  "url": "https://verify.didit.me/session/...",
  "session_id": "11111111-2222-3333-4444-555555555555",
  "session_token": "SESSION_TOKEN",
  "status": "Not Started"
}
```

```json theme={null}
"action_required": {
  "type": "wallet_ownership",
  "url": "https://verify.didit.me/wallet-ownership/3s9x...urlsafe",
  "widget_session_id": "77777777-8888-9999-aaaa-bbbbbbbbbbbb",
  "expires_at": "2026-07-07T13:00:00Z"
}
```

| 유형 | 나타나는 시점 | 사용자가 하는 작업 |
| - | - | - |
| `verification_session` | [규칙 작업](https://docs.didit.me/ko/transaction-monitoring/rules.md)에서 추가 사용자 인증이 필요해 거래가 `AWAITING_USER`로 전환된 경우입니다. | 호스팅 인증 세션(예: 생체 인증 재확인)을 완료합니다. 세션은 앱에서 실행합니다. 아래 [인증 세션 실행](#인증-세션-실행)을 참조하세요. |
| `wallet_ownership` | Travel Rule 이체에 최종 사용자가 지갑을 통제한다는 증명이 필요한 경우입니다. 목적지가 [자체 보관 지갑으로 선언된](#자체-보관-지갑-선언하기) 경우(`UNCONFIRMED_OWNERSHIP`)이거나, 송금 건에서 거래 상대방 VASP를 찾지 못한 경우입니다. 이때 [Travel Rule 설정](https://docs.didit.me/ko/transaction-monitoring/travel-rule.md#설정)에서 `auto_wallet_verification`이 활성화되어 있어야 합니다. Didit은 거래에 연결된 [지갑 소유권 위젯](https://docs.didit.me/ko/transaction-monitoring/wallet-ownership-widget.md) 세션을 자동으로 생성합니다. | Travel Rule 설정에서 허용하는 방식으로 지갑 통제권을 증명합니다. 지원되는 체인에서 메시지 서명, 스크린샷, 또는 자가 선언을 사용할 수 있습니다. 자동 생성된 세션에서는 Satoshi 테스트로 증명할 수 없습니다. 이 세션은 SDK가 자동으로 실행해 줍니다. |

한 거래에 두 경우가 모두 해당할 수 있다면 `wallet_ownership`이 우선합니다. 소유권 증명이 없으면 Travel Rule 교환 자체가 진행되지 않기 때문입니다.
대기 중인 작업이 해결되면 `action_required`는 `null`이 됩니다.
기존 `remediation` 필드는 하위 호환성을 위해 유지됩니다. 정식 블록은 `action_required`입니다.

### 자동 실행 계약

`autoLaunchAction`(기본값 `true`)은 `wallet_ownership` 작업만 자동으로 실행합니다.

* **웹** - SDK 고유의 오버레이 모달에서 열립니다. 완료 여부는 출처를 검증한 메시지 전달과 제한된 횟수의 거래 폴링으로 확인합니다. 단일 이벤트에만 의존하지 않습니다.
* **iOS / Android**(그리고 네이티브 SDK에 위임하는 React Native 및 Flutter 래퍼) - `useNativeWalletOwnership`이 `true`(기본값)이면 SDK의 **네이티브 지갑 소유권 화면**(SwiftUI / Jetpack Compose)으로 표시합니다. 이 화면을 사용할 수 없으면 `SFSafariViewController` 또는 Chrome Custom Tab으로, 그것도 안 되면 기본 브라우저로 대체합니다. 흐름이 닫히거나 앱이 포그라운드로 돌아오면 SDK가 거래를 자동으로 다시 조회합니다.

`verification_session` 작업은 자동으로 실행되지 않습니다. 항상 `action_required`로 반환되며, 앱이 자체 Didit 인증 연동으로 시작해야 합니다.

> [!WARNING]
>
> 지갑 소유권 위젯은 임베디드 모바일 웹뷰 안에서 절대 렌더링하면 안 됩니다. 지갑 딥링크가 웹뷰를 벗어날 수 없어 증명 흐름이 중단됩니다.
> SDK는 자동 실행 시 올바른 브라우저 화면을 이미 사용합니다. `url`을 직접 처리하는 경우에도 이 규칙을 유지하세요.
> 자세한 내용은 [지갑 소유권 위젯 가이드](https://docs.didit.me/ko/transaction-monitoring/wallet-ownership-widget.md#반환된-url-열기)를 참조하세요.

SDK가 자동으로 실행한 지갑 소유권 흐름이 완료되거나 닫히면, SDK는 거래를 다시 조회하고 갱신된 결과를 `onActionCompleted` 콜백으로 전달합니다.
`verification_session` 작업은 이 주기에 포함되지 않습니다. 앱이 직접 실행하기 때문이며, 자세한 내용은 다음 섹션을 참조하세요.

### 인증 세션 실행

`action_required.type`이 `verification_session`이면 `result.actionRequired`에서 블록을 읽고, 기존 Didit 인증 연동에 전달합니다.

```typescript Web theme={null}
if (result.actionRequired?.type === "verification_session") {
  DiditSdk.shared.startVerification({ url: result.actionRequired.url });
}
```

```swift iOS theme={null}
if actionRequired.type == "verification_session" {
    try await DiditSdk.shared.startVerification(token: actionRequired.sessionToken)
}
```

```kotlin Android theme={null}
if (actionRequired.type == "verification_session") {
    DiditSdk.startVerification(token = actionRequired.sessionToken) { result ->
        // handle the verification result
    }
}
```

```typescript React Native theme={null}
if (actionRequired.type === "verification_session") {
  await startVerification(actionRequired.sessionToken);
}
```

```dart Flutter theme={null}
if (actionRequired.type == 'verification_session') {
  await DiditSdk.startVerification(actionRequired.sessionToken);
}
```

인증 흐름이 끝나면 `GET /v1/transactions/{transaction_id}/`를 호출하거나 [거래 webhook](#웹훅)을 기다려 거래의 갱신된 상태를 확인하세요.

### 예시: 고액 출금 시 추가 생체 인증

이 흐름이 가장 효과적으로 쓰이는 경우는 위험한 거래, 즉 큰 금액, 새로운 수취인, 새로운 기기에 대해 온보딩 때 인증한 본인을 다시 생체 인증으로 확인하도록 요구하는 상황입니다.

계정 탈취는 바로 이런 모습입니다. 올바른 비밀번호, 올바른 2FA 코드(피싱 또는 SIM 스왑으로 탈취), 그리고 한 번도 본 적 없는 지갑으로의 출금입니다.
이 시점에는 자격 증명 기반의 방어가 모두 뚫린 상태입니다.
하지만 얼굴은 다릅니다. 공격자는 정당한 소유자가 KYC 온보딩 때 캡처한 얼굴과 일치해야 하는 라이브니스 감지를 거친 셀피를 통과할 수 없습니다.

**1. 콘솔에서 규칙을 한 번만 만드세요**

**Transactions → Rules → Create Rule**에서 조건은 `amount gte 5000`, 작업은 \*\*Change status → `AWAITING_USER`\*\*로 설정하고, 작업에서 **생체 인증** 워크플로를 선택하세요.
처음 보는 거래 상대방, 새로운 기기 지문, IP 국가 변경 같은 위험 조건은 추가 조건 또는 추가 규칙으로 자유롭게 넣을 수 있습니다.

**2. 평소처럼 출금을 제출하세요**

제출 코드는 바꿀 필요가 없습니다. 규칙이 발동하면 응답에 `status: "AWAITING_USER"`와 `verification_session` 유형의 `action_required` 블록이 포함되어 반환됩니다:

```json theme={null}
{
  "status": "AWAITING_USER",
  "action_required": {
    "type": "verification_session",
    "url": "https://verify.didit.me/session/...",
    "session_id": "11111111-2222-3333-4444-555555555555",
    "session_token": "SESSION_TOKEN",
    "status": "Not Started"
  }
}
```

**3. 재확인을 실행하세요**

`session_token`(또는 `url`)을 [인증 세션 실행](#인증-세션-실행)에서 설명한 것과 같이 기존 Didit 인증 연동에 전달하세요.
세션은 거래 대상자와 같은 `vendor_data`로 생성되므로, Didit은 온보딩에서 확인된 사용자의 기준 얼굴 이미지를 이미 보유하고 있습니다. 승인된 라이브니스 셀피, 여권 칩 사진, 또는 문서의 초상 사진이 여기에 해당합니다.
사용자는 라이브 셀피를 한 장 찍기만 하면 됩니다. Didit은 라이브니스 검사와 함께, 저장된 기준 얼굴과의 1:1 얼굴 대조를 수행합니다. 문서를 다시 스캔하거나 온보딩을 다시 할 필요는 없습니다. 위험한 거래에서만 몇 초의 번거로움이 추가됩니다.

**4. 결과에 따라 거래를 확정하세요**

세션이 완료되면 거래가 자동으로 다시 평가됩니다. 재확인을 통과하면 `APPROVED`, 실패하면 `DECLINED`가 되며, 두 경우 모두 `transaction.status.updated` webhook이 전송됩니다(거래 타임라인에는 `reason: remediation_completed`로 기록됩니다).
앱은 `onActionCompleted` 방식의 콜백, `GET /v1/transactions/{transaction_id}/`, 또는 webhook으로 결과를 받을 수 있습니다.

이 설계가 작동하는 이유는 KYC 온보딩, 생체 인증 재확인, 거래 결정이 하나의 플랫폼에서 실행되기 때문입니다. 온보딩 때 촬영한 얼굴이 추가 인증의 기준이 되고, 기기 그래프가 세션과 거래를 연결하며, 규칙 엔진이 이들을 하나로 묶습니다. 별도의 벤더 연동, 얼굴 이미지 내보내기, 별도의 생체 인식 계약이 필요하지 않습니다.
재확인은 실제로 실행될 때에만 표준 생체 인증 기능([\$0.10](https://docs.didit.me/ko/getting-started/pricing.md))으로 과금됩니다.

## 웹 SDK

`submitTransaction`은 현재 웹 SDK 싱글톤에서 사용할 수 있습니다:

```typescript theme={null}
import { DiditSdk } from "@didit-protocol/sdk-web";

const result = await DiditSdk.shared.submitTransaction({
  transactionToken: sdkToken, // minted by your backend via POST /v3/transactions/sdk-token/
  transaction: {
    txnId: "wd-2026-07-07-0042",
    txnDate: new Date().toISOString(),
    zoneId: "Europe/Madrid",
    category: "travelRule",
    details: {
      direction: "out",
      amount: "0.25",
      currency: "ETH",
      currencyType: "crypto",
      paymentDetails: "ETH withdrawal to self-hosted wallet",
      cryptoParams: { crypto_chain: "ETH" }
    },
    subject: {
      type: "individual",
      fullName: "Jane Doe"
      // no externalUserId needed: the subject identity comes from the token
    },
    counterparty: {
      type: "individual",
      fullName: "Ana Diaz",
      paymentMethod: {
        type: "unhosted_wallet",
        accountId: "0xBeneficiaryWallet01"
      }
    },
    travelRule: {
      status: "AWAITING_COUNTERPARTY", // ignored when Travel Rule is enabled: Didit drives the exchange
      beneficiaryData: {
        wallet_address: "0xBeneficiaryWallet01",
        name: "Ana Diaz"
      }
    },
    includeCryptoScreening: true
  },
  autoLaunchAction: true, // default: auto-launches wallet_ownership only; verification_session comes back in result.actionRequired for you to launch
  onActionCompleted: (refreshed) => {
    console.log(refreshed.status, refreshed.travelRuleStatus);
  }
});

console.log(result.transactionId, result.status, result.actionRequired?.type);
```

결과는 `{ transactionId, status, travelRuleStatus?, actionRequired? }` 형식입니다.
SDK 수준의 페이로드에서는 `category`, `details`, `customProperties`를 작성합니다. SDK가 이 값들을 와이어 별칭(`type`, `info`, `props`)에 자동으로 매핑합니다.

오류는 `DiditTransactionError`를 통해 타입이 지정됩니다:

```typescript theme={null}
import { DiditSdk, DiditTransactionError } from "@didit-protocol/sdk-web";

try {
  await DiditSdk.shared.submitTransaction({ transactionToken, transaction });
} catch (error) {
  if (error instanceof DiditTransactionError) {
    switch (error.type) {
      case "expired_token": // mint a fresh token and retry
        break;
      case "invalid_token": // rejected: unknown, revoked, or over max_uses
        break;
      case "validation": // inspect error.fieldErrors for per-field details
        break;
      case "network": // retry with backoff
        break;
    }
  }
}
```

## 네이티브 SDK

> [!NOTE]
>
> 네이티브 `submitTransaction` API는 iOS 및 Android SDK 4.1.0에 포함되어 제공됩니다(이 버전을 고정하는 React Native 및 Flutter 릴리스 포함).
> 웹 SDK와 달리 네이티브 페이로드는 와이어 필드 이름인 `type`, `info`, `props`를 그대로 사용합니다.

```swift Swift theme={null}
import DiditSDK

let payload = DiditTransactionPayload(
    txnId: "wd-2026-07-07-0042",
    type: "travelRule",
    info: DiditTransactionInfo(
        direction: "out",
        amount: 0.25,
        currency: "ETH",
        currencyType: "crypto",
        cryptoParams: ["crypto_chain": "ETH"]
    ),
    subject: DiditTransactionParticipant(type: "individual", fullName: "Jane Doe"),
    counterparty: DiditTransactionParticipant(
        type: "individual",
        fullName: "Ana Diaz",
        paymentMethod: DiditTransactionPaymentMethod(type: "unhosted_wallet", accountId: "0xBeneficiaryWallet01")
    ),
    travelRule: DiditTravelRuleInfo(
        status: "AWAITING_COUNTERPARTY", // required by the API; ignored when Travel Rule is enabled
        beneficiaryData: ["wallet_address": "0xBeneficiaryWallet01", "name": "Ana Diaz"]
    ),
    includeCryptoScreening: true
)

let result = try await DiditSdk.shared.submitTransaction(
    transactionToken: sdkToken,
    transaction: payload,
    options: DiditTransactionOptions(
        autoLaunchAction: true,           // default
        useNativeWalletOwnership: true,   // default: native flow, browser fallback
        onTransactionUpdated: { refreshed in
            print(refreshed.status ?? "", refreshed.travelRuleStatus ?? "")
        }
    )
)

print(result.transactionId, result.status ?? "", result.actionRequired?.type ?? "none")
// Errors: DiditTransactionError .invalidToken / .expiredToken / .validation(fieldErrors) / .network
```

```kotlin Kotlin theme={null}
import me.didit.sdk.DiditSdk
import me.didit.sdk.transactions.*

val payload = DiditTransactionPayload(
    txnId = "wd-2026-07-07-0042",
    type = "travelRule",
    info = DiditTransactionInfo(
        direction = "out",
        amount = 0.25,
        currency = "ETH",
        currencyType = "crypto",
        cryptoParams = mapOf("crypto_chain" to "ETH")
    ),
    subject = DiditTransactionParticipant(type = "individual", fullName = "Jane Doe"),
    counterparty = DiditTransactionParticipant(
        type = "individual",
        fullName = "Ana Diaz",
        paymentMethod = DiditTransactionPaymentMethod(
            type = "unhosted_wallet",
            accountId = "0xBeneficiaryWallet01"
        )
    ),
    travelRule = DiditTravelRule(
        status = "AWAITING_COUNTERPARTY", // required by the API; ignored when Travel Rule is enabled
        beneficiaryData = mapOf("wallet_address" to "0xBeneficiaryWallet01", "name" to "Ana Diaz")
    ),
    includeCryptoScreening = true
)

// suspend fun - call from a coroutine; DiditSdk.initialize() must have run first
val result = DiditSdk.submitTransaction(
    transactionToken = sdkToken,
    transaction = payload,
    options = DiditTransactionOptions(
        autoLaunchAction = true,          // default
        useNativeWalletOwnership = true,  // default: native flow, Custom Tab fallback
        onTransactionUpdated = { refreshed ->
            Log.d("Didit", "${refreshed.status} ${refreshed.travelRuleStatus}")
        }
    )
)

Log.d("Didit", "${result.transactionId} ${result.status} ${result.actionRequired?.type}")
// Errors: sealed DiditTransactionException - InvalidToken / ExpiredToken / Validation(fieldErrors) / Network
```

```typescript React Native theme={null}
import {
  submitTransaction,
  getTransaction,
  DiditTransactionError,
} from "@didit-protocol/sdk-react-native";

const result = await submitTransaction(sdkToken, {
  txnId: "wd-2026-07-07-0042",
  type: "travelRule",
  info: {
    direction: "out",
    amount: 0.25,
    currency: "ETH",
    currencyType: "crypto",
    cryptoParams: { crypto_chain: "ETH" },
  },
  subject: { type: "individual", fullName: "Jane Doe" },
  counterparty: {
    type: "individual",
    fullName: "Ana Diaz",
    paymentMethod: { type: "unhosted_wallet", accountId: "0xBeneficiaryWallet01" },
  },
  travelRule: {
    status: "AWAITING_COUNTERPARTY", // required by the API; ignored when Travel Rule is enabled
    beneficiaryData: { wallet_address: "0xBeneficiaryWallet01", name: "Ana Diaz" },
  },
  includeCryptoScreening: true,
}, {
  autoLaunchAction: true, // default
  onTransactionUpdated: (refreshed) => {
    console.log(refreshed.status, refreshed.travelRuleStatus);
  },
});

console.log(result.transactionId, result.status, result.actionRequired?.type);
// Errors: DiditTransactionError with code invalid_token | expired_token | validation | network
```

```dart Flutter theme={null}
import 'package:didit_sdk/sdk_flutter.dart';

final result = await DiditSdk.submitTransaction(
  sdkToken,
  DiditTransactionPayload(
    txnId: 'wd-2026-07-07-0042',
    type: 'travelRule',
    info: DiditTransactionInfo(
      direction: 'out',
      amount: 0.25,
      currency: 'ETH',
      currencyType: 'crypto',
      cryptoParams: {'crypto_chain': 'ETH'},
    ),
    subject: DiditTransactionParticipant(type: 'individual', fullName: 'Jane Doe'),
    counterparty: DiditTransactionParticipant(
      type: 'individual',
      fullName: 'Ana Diaz',
      paymentMethod: DiditTransactionPaymentMethod(
        type: 'unhosted_wallet',
        accountId: '0xBeneficiaryWallet01',
      ),
    ),
    travelRule: DiditTravelRule(
      status: 'AWAITING_COUNTERPARTY', // required by the API; ignored when Travel Rule is enabled
      beneficiaryData: {'wallet_address': '0xBeneficiaryWallet01', 'name': 'Ana Diaz'},
    ),
    includeCryptoScreening: true,
  ),
  options: DiditTransactionOptions(
    autoLaunchAction: true, // default
    onTransactionUpdated: (refreshed) {
      debugPrint('${refreshed.status} ${refreshed.travelRuleStatus}');
    },
  ),
);

debugPrint('${result.transactionId} ${result.status} ${result.actionRequired?.type}');
// Errors: DiditTransactionException with code invalid_token | expired_token | validation | network
```

모든 SDK는 같은 토큰으로 거래를 다시 읽을 수 있도록 `getTransaction(transactionToken, transactionId)`도 제공합니다. SDK는 실행된 작업 흐름 이후에 이 함수를 자동으로 호출하며, 필요한 시점에 직접 호출할 수도 있습니다.

모바일에서는 기기 지문 전송 방식이 모든 SDK 네트워크 호출에 이미 적용되어 있으므로, 거래 제출에도 같은 기기 인텔리전스가 자동으로 포함됩니다.

## SDK 없이 사용하기

SDK가 하는 모든 일은 일반 HTTPS로 수행할 수 있으며, 여기에 SDK가 필요하지는 않습니다.
동등한 연동 방식은 두 가지입니다.

**서버 측 전용(토큰 전혀 사용하지 않음).** 백엔드에서 API 키를 사용해 [`POST /v3/transactions/`](https://docs.didit.me/ko/management-api/transactions/create)로 제출합니다. 응답과 이후의 모든 `GET /v3/transactions/{transaction_id}/` 응답에는 같은 `action_required` 블록이 포함됩니다. SDK가 기기 정보를 수집해 주지 않으므로, 참여자별로 명시적인 `device` 컨텍스트를 전달할 수도 있습니다.

**클라이언트 측, 직접 구성한 HTTP 스택 사용.** [`POST /v3/transactions/sdk-token/`](https://docs.didit.me/ko/management-api/transactions/sdk-token)으로 토큰을 발급받은 다음, 기기에서 `X-Transaction-Token` 헤더와 함께 [`POST /v1/transactions/`](https://docs.didit.me/ko/management-api/transactions/submit-sdk) 및 [`GET /v1/transactions/{transaction_id}/`](https://docs.didit.me/ko/management-api/transactions/get-sdk)를 호출합니다. 이때 같은 주체 바인딩과 서버 측 IP/사용자 에이전트 도출이 그대로 적용됩니다.

그다음 `action_required`는 직접 처리해야 합니다:

| `action_required.type` | 할 일 |
| - | - |
| `wallet_ownership` | 사용자에게 `url`을 전달하고 **실제 브라우저 화면**에서 엽니다. 웹에서는 일반 탭, 모바일에서는 `SFSafariViewController` 또는 Chrome Custom Tabs를 사용하거나, 이메일 링크나 QR 코드로 별도로 전달합니다. 지갑 딥링크는 임베디드 웹뷰를 벗어날 수 없으므로, 절대 임베디드 웹뷰 안에 렌더링하지 마십시오. |
| `verification_session` | 사용자를 `url`로 이동시킵니다. 이 URL은 표준 Didit 호스팅 인증 페이지입니다. 또는 이미 운영 중인 Didit 인증 연동에 `session_token`을 전달할 수 있습니다. |

클라이언트 콜백 없이 완료 여부를 확인하는 방법은 다음과 같습니다:

* **웹훅(권장).** 대기 중인 작업이 거래 처리를 완료하면 `transaction.status.updated`가 발생하고, Travel Rule의 모든 상태 전환에서 `travel_rule.status.updated`가 발생합니다. 두 이벤트 모두 폴링 없이 기록을 업데이트하기에 충분한 정보를 담고 있습니다.
* **폴링.** 거래를 다시 조회합니다(백엔드에서는 `/v3/`, 토큰이 유효한 동안에는 `/v1/`을 사용합니다). 해결되면 `action_required`가 `null`로 돌아가고, `status`와 `travel_rule.status`에 결과가 담깁니다.

이 설계에는 일회성 의존성이 없습니다. 위젯, 인증 세션, 백엔드가 모두 같은 영구적인 거래 상태로 수렴하므로, 닫힌 탭이나 놓친 웹훅이 있더라도 거래를 다시 조회하면 항상 복구할 수 있습니다.

## 웹훅

백엔드가 결과를 보고받기 위해 앱이 필요한 것은 아닙니다.
제출된 거래에 사용자 작업이 필요하면 [거래 웹훅](https://docs.didit.me/ko/transaction-monitoring/webhooks.md)(`transaction.created` 또는 `transaction.status.updated`)에 같은 `action_required` 블록이 포함됩니다. 따라서 흐름이 끝나기 전에 앱을 닫더라도 시스템이 대기 중인 작업을 알 수 있습니다.
`verification_session` 작업에는 `AWAITING_USER` 상태가 함께 제공되지만, `wallet_ownership` 작업은 그 자체로 거래 상태를 바꾸지 않습니다. 따라서 상태 대신 `action_required`를 확인하십시오.
Travel Rule 상태가 전환되면 [`travel_rule.status.updated`](https://docs.didit.me/ko/transaction-monitoring/travel-rule#webhook)도 추가로 발생합니다.

## 다음 단계

> [card] [거래 제출](https://docs.didit.me/ko/transaction-monitoring/transactions.md)
>
> 서버 측 Create Transaction API와 전체 페이로드 참조 문서입니다.

> [card] [Travel Rule](https://docs.didit.me/ko/transaction-monitoring/travel-rule.md)
>
> 관리형 송금인-수취인 정보 교환, 설정 및 수명 주기입니다.

> [card] [지갑 소유권 위젯](https://docs.didit.me/ko/transaction-monitoring/wallet-ownership-widget.md)
>
> wallet\_ownership 작업의 기반이 되는 호스팅 제어 권한 증명 흐름입니다.

> [card] [웹훅](https://docs.didit.me/ko/transaction-monitoring/webhooks.md)
>
> transaction.created, transaction.status.updated 및 서명 검증입니다.

> [card] [규칙](https://docs.didit.me/ko/transaction-monitoring/rules.md)
>
> 규칙 작업, 인증 세션을 생성하는 작업을 포함합니다.

> [card] [상태](https://docs.didit.me/ko/transaction-monitoring/statuses.md)
>
> 거래 수명 주기에서 AWAITING\_USER가 의미하는 바입니다.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.