HTTP API¶
기본 주소는 http://localhost:3000입니다. 모든 응답은 JSON이며 세션은 HttpOnly
쿠키 umul_session으로 유지합니다.
인증¶
POST /v1/auth/nonce¶
{ "address": "0xEf792c4973Cc8ef6aF433B3bd9f4461B701301f6" }
응답은 서명할 SIWE 메시지와 만료 시각입니다. nonce는 5분 후 만료하고 한 번만 사용할 수 있습니다. 저장되는 값은 원문이 아니라 SHA-256 해시입니다.
{ "message": "localhost:5173 wants you to sign in ...", "expiresAt": "..." }
POST /v1/auth/verify¶
{ "message": "<위에서 받은 메시지>", "signature": "0x..." }
서명에서 복구한 주소가 메시지의 주소와 다르면 거부합니다. 성공하면
Set-Cookie: umul_session=...; HttpOnly; SameSite=Lax를 내려줍니다.
DELETE /v1/auth/session¶
세션을 폐기하고 쿠키를 만료시킵니다. 세션이 없어도 204를 반환합니다.
| 오류 코드 | 상태 |
|---|---|
INVALID_REQUEST |
400 |
INVALID_WALLET_ADDRESS, INVALID_LOGIN_MESSAGE, LOGIN_MESSAGE_MISMATCH |
400 |
INVALID_WALLET_SIGNATURE, LOGIN_NONCE_UNKNOWN |
401 |
LOGIN_ORIGIN_NOT_ALLOWED |
403 |
LOGIN_NONCE_ALREADY_USED |
409 |
LOGIN_NONCE_EXPIRED |
410 |
INVALID_REQUEST는 요청 본문이 스키마에 맞지 않을 때입니다.
증빙¶
모든 증빙 엔드포인트는 유효한 세션을 요구합니다. 세션이 없으면 401
SESSION_REQUIRED를 반환합니다. 참여자가 아닌 주소에는 존재 여부를 알리지 않고
404 EVIDENCE_REQUEST_NOT_FOUND를 반환합니다.
POST /v1/evidence-requests¶
Idempotency-Key 헤더가 필요합니다. 같은 키로 다시 요청하면 새 요청을 만들지
않고 기존 요청을 돌려줍니다.
{
"name": "계약 조건 합의",
"messageRefs": ["<xmtp-message-id>", "..."],
"evidenceId": "0x...",
"contentHash": "0x...",
"conversationIdHash": "0x...",
"participantsHash": "0x...",
"manifestVersion": "gasok-evidence-v1",
"messageCount": 12,
"conversationStartedAt": "2026-07-26T00:00:00.000Z",
"conversationEndedAt": "2026-07-26T01:00:00.000Z",
"participants": ["0x...", "0x..."]
}
모든 필드가 필수이고 정의되지 않은 필드는 거부합니다.
| 필드 | 제약 |
|---|---|
name |
1~100자. 온체인에 올라가지 않는 사람이 읽는 라벨 |
messageRefs |
XMTP 메시지 ID 1~4096개. 본문은 받지 않습니다. 개수가 messageCount와 같아야 합니다 |
manifestVersion |
1~64자 |
messageCount |
1 이상 |
participants |
2~32개 주소 |
세션 주소가 participants에 없으면 403입니다. 메시지 원문은 받지 않습니다.
GET /v1/evidence-requests¶
세션 주소가 참여자인 요청만 최신순으로 반환합니다. 저장한 것이 먼저 옵니다.
GET /v1/evidence-requests/:id¶
응답에는 name, messageRefs와 함께 saved가 들어갑니다. saved는 요청한
참여자 본인의 저장 여부이므로 같은 증빙이라도 사람마다 값이 다릅니다. 서명과
참여자 주소는 노출하되 세션 토큰과 메시지 원문은 담지 않습니다.
POST /v1/evidence-requests/:id/save¶
증빙을 저장(즐겨찾기)하거나 해제합니다. 참여자별 상태라 한 사람이 저장해도 다른 참여자의 목록에는 영향이 없습니다.
{ "saved": true }
POST /v1/evidence-requests/:id/signatures¶
{ "signature": "0x<130 hex>" }
서명자는 세션 주소로 결정합니다. 요청 본문에 주소를 넣을 수 없습니다.
POST /v1/evidence-requests/:id/rejection¶
POST /v1/evidence-requests/:id/submission¶
{ "transactionHash": "0x...", "outcome": "confirmed", "blockNumber": 31763659 }
outcome을 생략하면 SUBMITTED까지만 기록합니다.
POST /v1/evidence-requests/:id/archive¶
{ "ciphertext": "<base64>" }
증빙 원문의 아카이브 업로드입니다. 클라이언트가 AES-256-GCM 으로 잠근 암호문만
받고, 콘텐츠 키는 XMTP 제어 메시지(E2EE)로만 오가므로 서버는 열 수 없습니다.
제안자가 아니면 403 EVIDENCE_ARCHIVE_FORBIDDEN입니다. 최초 1회만 저장되고,
같은 내용의 재시도는 성공, 다른 내용은 409 EVIDENCE_ARCHIVE_CONFLICT다.
암호문은 GCM 봉투 최소 길이(29바이트)에서 2MB까지 받습니다.
POST /v1/evidence-requests/:id/archive/keys¶
{ "wrappedKey": "<base64 80자>" }
서명을 마친 참여자가 자신의 래핑된 콘텐츠 키(60바이트 고정)를 맡깁니다.
래핑 키는 지갑 서명에서 파생한 키로 잠근 값이라 서버는 열 수 없습니다. 서명
전에는 403 EVIDENCE_ARCHIVE_FORBIDDEN입니다. 저장 규칙은 archive 와 같다
(first-write-wins, 다른 값 409).
GET /v1/evidence-requests/:id/archive¶
{ "ciphertext": "<base64>", "wrappedKey": "<base64|null>" }
서명한 참여자만 암호문과 자신의 래핑 키를 받습니다. 서명하지 않은 참여자와
아카이브가 없는 요청에는 404 EVIDENCE_ARCHIVE_NOT_FOUND를 반환해 존재
여부를 알리지 않습니다. wrappedKey가 null 이면 키 등록에 실패한 서명자입니다.
증빙 상태¶
PROPOSED -> SIGNING -> READY -> SUBMITTED -> CONFIRMED
| | | |
+------> REJECTED +------> FAILED
+------> EXPIRED
FAILED에서는 수집한 서명을 유지한 채 재제출할 수 있습니다.
| 오류 코드 | 상태 |
|---|---|
INVALID_REQUEST, IDEMPOTENCY_KEY_REQUIRED |
400 |
EVIDENCE_HASH_MALFORMED, EVIDENCE_SIGNATURE_MALFORMED, EVIDENCE_ARCHIVE_MALFORMED |
400 |
EVIDENCE_NAME_INVALID, EVIDENCE_MESSAGE_REFS_MISMATCH, EVIDENCE_TIME_RANGE_INVALID |
400 |
EVIDENCE_PARTICIPANTS_TOO_FEW, EVIDENCE_PARTICIPANTS_DUPLICATED |
400 |
EVIDENCE_REQUESTER_NOT_PARTICIPANT |
400 |
SESSION_REQUIRED |
401 |
EVIDENCE_PARTICIPANT_UNKNOWN, EVIDENCE_ARCHIVE_FORBIDDEN |
403 |
EVIDENCE_REQUEST_NOT_FOUND, EVIDENCE_ARCHIVE_NOT_FOUND |
404 |
EVIDENCE_SIGNATURE_ALREADY_SUBMITTED, EVIDENCE_ARCHIVE_CONFLICT |
409 |
EVIDENCE_REQUEST_NOT_READY, EVIDENCE_REQUEST_NOT_SIGNABLE |
409 |
EVIDENCE_REQUEST_NOT_REJECTABLE, EVIDENCE_REQUEST_NOT_SUBMITTED |
409 |
EVIDENCE_REQUEST_EXPIRED |
410 |
EVIDENCE_REQUESTER_NOT_PARTICIPANT은 403이 아니라 400입니다. 제안자가
자신을 participants에 넣지 않은 것은 권한 문제가 아니라 잘못된 요청입니다.
헬스체크¶
GET /health는 { "status": "ok", "time": "<ISO 8601>" }을 반환합니다.