← 뒤로가기 throwDocs API v1

throwDocs API
문서 등록부터 링크 회수까지

PDF를 등록해 공유 링크를 만들고, 열람 상태 조회·회수·웹훅까지 연동하는 v1 규격입니다. 고객 발송은 연동 시스템이 담당합니다.

Base https://doc.throw.im/api/v1 OpenAPI YAML 내려받기

개요

비즈니스 플랜 전용입니다. 다른 플랜의 호출은 plan_upgrade_required 403으로 거절됩니다.

연동하는 쪽이 하는 일throw가 하는 일
PDF와 고객 정보를 보냅니다공유 링크를 반환합니다
메일·문자 등 기존 채널로 링크를 발송합니다뷰어에서 만료·워터마크·다운로드 정책을 적용합니다
상태를 조회하거나 링크를 회수합니다열람·다운로드 횟수를 기록합니다

이 API가 하지 않는 일

  • 메일·문자 발송
  • externalRef로 받은 고객 정보의 해석
  • 문서를 보내는 담당자의 계정 생성
요청 주체

요청은 토큰 주인 명의로 처리됩니다. 담당자별 명의가 필요하면 각 계정에서 토큰을 발급해 보관하세요.

빠른 시작

토큰 확인부터 공유 링크 생성까지 세 번의 호출로 끝납니다.

터미널 없이 먼저 눌러보고 싶다면

API 점검창에서 같은 요청을 보낼 수 있습니다.

  1. 토큰 발급마이페이지 → API 연동. 원문은 한 번만 표시됩니다.
  2. 연결 확인GET /me로 토큰과 사용 가능 옵션을 확인합니다.
  3. 공유 생성PDF를 등록한 뒤 받은 documentId로 공유를 만듭니다.
# 토큰과 API 주소
export TOKEN="tdk_live_여기에_붙여넣기"
export API=https://doc.throw.im/api/v1

# ① 연결 확인
curl -s $API/me -H "Authorization: Bearer $TOKEN"

# ② 파일 등록
DOC=$(curl -s -X POST $API/documents \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@계약서.pdf;type=application/pdf" | jq -r .documentId)

# ③ 공유 생성
curl -s -X POST $API/shares \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d "{
    \"documentId\": \"$DOC\",
    \"recipient\": { \"name\": \"김민수\", \"email\": \"minsu@example.com\" }
  }"
다른 입력 방식

S3 URL이나 파일을 POST /shares에 바로 보내는 방법은 공유 생성에서 확인하세요.

links.recipient는 고객에게 보내고, links.host는 담당자 화면에 저장해 같은 주소를 계속 사용합니다.

오류가 나면 오류 코드에서 원인별 해결 방법을 확인하세요.

API 점검

토큰 연결만 빠르게 확인합니다.

별도 샌드박스나 테스트 토큰은 없습니다

점검창과 https://doc.throw.im/api/v1 요청은 모두 실제 환경에서 실행됩니다. 비즈니스 플랜 토큰이 필요하며 문서·공유 생성은 저장 용량과 호출 한도에 반영됩니다. 테스트에는 GET /me로 확인한 가장 짧은 유효기간을 쓰고, 끝나면 테스트 공유를 회수하세요.

연동 설계

파일 재사용 여부에 따라 호출 방식을 고르고, 응답 식별자를 저장합니다.

문서와 공유의 차이

문서는 파일, 공유는 수신자별 링크입니다.

POST /documentsPOST /shares
생성 단위파일당 하나수신자당 하나
멱등 키안 받습니다필수
형식multipart 또는 JSON(url)JSON · 파일을 실으면 multipart
호출 방식 선택

같은 파일을 여러 명에게: POST /documents 한 번 + 수신자별 POST /shares.

파일이 매번 다르면: POST /shares에 file 또는 url을 바로 보냅니다.

저장할 값

값왜 필요한가
id (shr_…)상태 조회와 회수
document.id (doc_…)같은 파일 재사용
links.recipient고객 발송·재안내
links.host담당자 재입장
사용한 토큰다른 계정의 토큰으로 조회하면 404입니다
id와 멱등 키를 발송 기록에 함께 저장하세요

응답을 놓쳤다면 24시간 안에 같은 멱등 키와 같은 요청으로 다시 호출해 결과를 재생할 수 있습니다. 24시간이 지났거나 멱등 키도 잃어버리면 목록·externalRef 검색 API가 없어 shr_…를 복구할 수 없습니다.

발송 기록에 멱등 키·공유 id·document.id·links.recipient·links.host·사용한 토큰을 함께 저장하세요. 현재 공유 상태는 필요할 때 GET /shares/{id}로 다시 확인합니다.

API로 생성한 links.host는 로그인 없이 열리는 짧은 주소입니다. 반환된 주소를 그대로 저장하고 재사용하세요. 주소를 가진 사람은 해당 문서의 호스트로 입장하므로 고객에게 전달하지 마세요.

구현 핵심

복사 가능한 요청 예시는 각 엔드포인트에 있습니다. 운영 코드에는 아래 규칙을 적용하세요.

항목규칙
Idempotency-Key발송 기록 ID를 쓰고 재시도에도 같은 값을 보냅니다
재시도429는 Retry-After 후, 5xx는 지수 백오프로 재시도합니다
409request_in_progress만 재시도하고 duplicate_request는 요청 오류로 처리합니다
오류 기록error.code와 X-Request-Id를 함께 남깁니다
대량 발송쓰기 5건/분을 지키고 같은 파일은 documentId를 재사용합니다

사용량 제한

대량 발송은 토큰과 계정 한도를 모두 고려합니다.

구분토큰 하나한 계정 전체대상
쓰기5 / 분15 / 분POST · DELETE
읽기60 / 분180 / 분GET
쓰기 요청에 포함되는 호출

POST /documents도 쓰기 1건입니다. 파일을 POST /shares에 바로 보내면 고객당 1건이고, 같은 파일은 documentId를 재사용해 전송량을 줄일 수 있습니다.

실제 계산

토큰 하나의 쓰기 5건/분 기준입니다.

상황필요 호출걸리는 시간
고객 1명 (문서 등록 + 공유)2건즉시
고객 10명 (파일을 공유 요청에 포함)10건약 1분
같은 문서를 고객 100명에게100건약 19분

요청 한도 초과

HTTP 429
Retry-After: 42                // 42초 뒤에 다시 오세요
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 42

{ "error": { "code": "rate_limited",
            "details": { "retryAfterSeconds": 42,
                         "scope": "token" } } }
scope뜻대응
token토큰 한도Retry-After 후 재시도
user계정 전체 한도토큰을 늘리지 말고 한도 회복까지 대기

엔드포인트

모든 요청에 Authorization: Bearer {토큰} 헤더가 필요합니다.

GET/me

토큰의 권한과 사용 가능한 공유 옵션을 조회합니다.

응답

잘못되거나 회수된 토큰은 401입니다.

{
  "actor": { "kind": "pat",
              "scopes": ["documents:write", "shares:write", "shares:read"] },
  "plan": {
    "code": "business",
    "maxExpiresInDays": 365,
    "allowedExpiresInDays": [1, 7, 90, 365],
    "maxShareRecipients": null,
    "maxUploadBytes": 262144000,
    "screenGuardAvailable": true
  }
}
필드타입설명
actor.scopesstring[] 토큰 권한. 권한이 없으면 missing_scope
maxExpiresInDaysinteger 유효기간 상한
allowedExpiresInDaysinteger[] expiresInDays에 넣을 수 있는 값
maxShareRecipientsinteger 웹 화면용 값. API 공유는 수신자당 한 번 호출합니다
maxUploadBytesinteger 파일 하나의 최대 바이트. 초과 시 file_too_large
screenGuardAvailableboolean 화면 이탈 보호 사용 가능 여부
POST/documentsdocuments:write

file 또는 공개 HTTPS url로 PDF를 등록하고 documentId를 받습니다. 둘 중 하나만 보내세요.

요청

파라미터타입설명
filefile multipart/form-data의 PDF 파일
urlstring PDF가 있는 공개 HTTPS 주소. S3 사전 서명 URL도 사용할 수 있습니다
namestring url의 저장 파일명. 생략 시 URL 경로 사용

url로 보낼 때

정책값넘으면
크기 상한요금제 상한과 250MB 중 작은 쪽 file_too_large 413
응답 대기15초document_fetch_failed
리다이렉트최대 3홉 사설 주소로 이동하면 차단
형식 판정앞 바이트 Content-Type 대신 %PDF- 확인
# 파일을 가져옵니다. S3 사전 서명 URL을 그대로 보내도 됩니다.
curl -X POST $API/documents \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://버킷.s3.ap-northeast-2.amazonaws.com/docs/계약서.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260901%2Fap-northeast-2%2Fs3%2Faws4_request&X-Amz-Date=20260901T091500Z&X-Amz-Expires=1800&X-Amz-SignedHeaders=host&X-Amz-Signature=b1d7c0f4e2a95c3d8f6b04e1a7c92d5f38ba61c4e07d92f5a3b8c61d4f290e7b",
       "name":"계약서.pdf"}'

응답

name과 createdAt은 실제로 반환된 문서를 기준으로 합니다.

{ "documentId": "doc_pJbchjs_mQ4QYKrkjTDkiA",
  "name": "계약서.pdf",
  "sizeBytes": 148231,
  "createdAt": "2026-08-27T00:00:00Z" }

같은 파일을 다시 올릴 때

POST /documents는 Idempotency-Key를 받지 않습니다. 대신 같은 계정이 같은 내용·같은 이름을 1분 안에 다시 올리면 앞서 등록된 문서를 반환합니다.

상황결과
1분 안에 같은 파일·같은 이름같은 documentId
1분이 지난 뒤 같은 파일·같은 이름새 문서
같은 파일·다른 이름새 문서
문서를 지운 뒤 다시 올림새 문서
createdAt으로 재사용 여부를 확인하세요

재사용된 경우 처음 등록된 시각이 반환됩니다. 그 문서의 기존 공유는 GET /documents/{id}의 activeShares에서 확인하세요.

같은 파일을 다시 보낼 때는 documentId 재사용을 권장합니다

1분 재사용은 응답을 받지 못해 즉시 재시도할 때를 위한 안전장치입니다. 일반적인 재발송은 저장해 둔 documentId로 POST /shares를 호출하세요.

같은 이름이 이미 있을 때

새 문서의 이름 뒤에 번호가 붙습니다. 화면에 표시할 이름은 요청값이 아닌 응답의 name을 사용하세요.

계약서.pdf → 계약서 (2).pdf → 계약서 (3).pdf

파일 내용은 저장소에서 중복 제거되더라도 문서는 위 규칙에 따라 구분됩니다.

실패하면 unsupported_file_type · file_too_large · storage_quota_exceeded · file_required(파일도 url도 없음) · document_fetch_failed(url을 받지 못함) 중 하나가 돌아옵니다.

GET/documents/{id}documents:write

등록한 문서와 활성 공유 수를 조회합니다.

응답

{ "documentId": "doc_pJbchjs_mQ4QYKrkjTDkiA",
  "name": "계약서.pdf",
  "sizeBytes": 148231,
  "createdAt": "2026-08-27T00:00:00Z",
  "activeShares": 3 }                      // 살아 있는 공유 수

activeShares가 0이어야 force 없이 삭제할 수 있습니다.

DELETE/documents/{id}documents:write

문서를 휴지통으로 보냅니다.

활성 공유가 있으면 삭제할 수 없습니다

409 document_in_use와 details.activeShares가 반환됩니다. 공유를 먼저 회수하거나, 링크가 즉시 무효화되는 것을 확인한 뒤 ?force=true를 사용하세요.

평소
DELETE /api/v1/documents/doc_pJbchjs_mQ4QYKrkjTDkiA

살아 있는 공유가 있어도 지울 때
DELETE /api/v1/documents/doc_pJbchjs_mQ4QYKrkjTDkiA?force=true

응답

{ "id": "doc_pJbchjs_mQ4QYKrkjTDkiA",
  "deleted": true,
  "activeShares": 0,                          // force 로 지웠다면 방금 죽은 링크 수
  "restorableUntil": "2026-09-09T00:00:00Z" }
필드타입설명
activeSharesinteger 삭제 시점의 활성 공유 수
restorableUntilstring 마이페이지에서 복구할 수 있는 기한

파일은 7일 뒤 영구 삭제됩니다. 그 전에는 마이페이지에서 복구할 수 있습니다.

POST/sharesshares:write

공유를 만들고 고객용 links.recipient와 담당자용 links.host를 반환합니다. 고객 링크 발송은 연동 시스템이 담당합니다.

file·url·documentId 중 하나만 보내세요.

방법보내는 것형식호출 수이럴 때
①filemultipart1건 파일을 서버에 들고 있을 때
②urlJSON1건 파일이 S3 등 주소로 열릴 때
③documentIdJSON1건 같은 파일을 여러 명에게 보낼 때
보낸 파일은 담당자 문서함에 남습니다

파일은 토큰 주인의 문서함에 등록됩니다. 같은 파일은 document.id를 재사용하고, 정리는 DELETE /documents/{id}에서 합니다.

각 탭에는 문서 지정부터 발신자·수신자·부가 정보·공유 옵션·안내 문구까지 한 번에 보낼 전체 요청을 담았습니다.

① file — 파일을 직접 올린다 · Content-Type: multipart/form-data

{ "file": (계약서.pdf 의 바이너리),
  "sender": {
    "name": "박영업",
    "email": "yh.park@company.com"
  },
  "recipient": {
    "name": "김민수",
    "email": "minsu@example.com",
    "phone": "010-1234-5678",
    "externalRef": "cus_1001",
    "metadata": {
      "contractId": "c-99",
      "branch": "강남",
      "staffId": "emp-042"
    }
  },
  "options": {
    "expiresInDays": 90,
    "personalNote": true,
    "branding": true,
    "watermark": false,
    "allowDownload": true,
    "screenGuard": false
  },
  "message": "{{recipient.name}}님, 요청하신 계약서를 보내드립니다. — {{sender.name}}"
}

multipart 의 폼 필드로 보냅니다 — 필드명은 위 키 그대로입니다.
객체인 항목(recipient·sender·options)은 JSON 문자열로 싣습니다.
② url — 파일이 있는 주소를 준다 · Content-Type: application/json

{ "url": "https://버킷.s3.ap-northeast-2.amazonaws.com/docs/계약서.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260901%2Fap-northeast-2%2Fs3%2Faws4_request&X-Amz-Date=20260901T091500Z&X-Amz-Expires=1800&X-Amz-SignedHeaders=host&X-Amz-Signature=b1d7c0f4e2a95c3d8f6b04e1a7c92d5f38ba61c4e07d92f5a3b8c61d4f290e7b",
  "name": "계약서.pdf",
  "sender": {
    "name": "박영업",
    "email": "yh.park@company.com"
  },
  "recipient": {
    "name": "김민수",
    "email": "minsu@example.com",
    "phone": "010-1234-5678",
    "externalRef": "cus_1001",
    "metadata": {
      "contractId": "c-99",
      "branch": "강남",
      "staffId": "emp-042"
    }
  },
  "options": {
    "expiresInDays": 90,
    "personalNote": true,
    "branding": true,
    "watermark": false,
    "allowDownload": true,
    "screenGuard": false
  },
  "message": "{{recipient.name}}님, 요청하신 계약서를 보내드립니다. — {{sender.name}}"
}
③ documentId — 미리 등록해 둔 문서를 쓴다 · Content-Type: application/json

{ "documentId": "doc_pJbchjs_mQ4QYKrkjTDkiA",
  "sender": {
    "name": "박영업",
    "email": "yh.park@company.com"
  },
  "recipient": {
    "name": "김민수",
    "email": "minsu@example.com",
    "phone": "010-1234-5678",
    "externalRef": "cus_1001",
    "metadata": {
      "contractId": "c-99",
      "branch": "강남",
      "staffId": "emp-042"
    }
  },
  "options": {
    "expiresInDays": 90,
    "personalNote": true,
    "branding": true,
    "watermark": false,
    "allowDownload": true,
    "screenGuard": false
  },
  "message": "{{recipient.name}}님, 요청하신 계약서를 보내드립니다. — {{sender.name}}"
}

모든 방식에 Authorization과 Idempotency-Key가 필요합니다.

multipart로 보낼 때는 객체 항목을 JSON 문자열로

recipient·sender·options는 JSON 문자열로 보내세요.

파일 전송도 안전하게 재시도할 수 있습니다

같은 키와 파일은 기존 결과를 재생하고, 같은 키에 다른 파일은 409 duplicate_request로 거절합니다.

요청 헤더

헤더필수설명
Authorization필수 Bearer {발급받은 토큰} 형식입니다.
Idempotency-Key필수 발송 기록 ID를 권장합니다. 재시도에는 같은 값을 사용하며 재생 기간은 24시간입니다.

요청 본문

PDF는 file, documentId, url 중 한 가지 방법으로 전달하세요. 점이 붙은 이름은 객체 내부 필드를 뜻합니다.

파라미터타입설명
filefile multipart/form-data의 PDF 파일
documentIdstring 공유할 문서 ID입니다. POST /documents로 받습니다.
urlstring PDF가 있는 공개 HTTPS 주소. S3 사전 서명 URL도 사용할 수 있습니다
namestring url의 저장 파일명. 생략 시 URL 경로 사용
senderobject 고객 화면에 표시할 담당자. 생략 시 docs 계정 이름 사용
sender.namestring 표시할 담당자 이름. throw는 이 값을 인증하지 않습니다
sender.emailstring 담당자 이메일
recipient 필수object 수신자 객체. email 또는 phone 중 하나 이상을 입력하세요. 생략하거나 null이면 recipient_required
recipient.namestring 뷰어에 표시할 이름
recipient.emailstring 수신자 이메일
recipient.phonestring 수신자 전화번호. 형식은 검증하지 않습니다
recipient.metadataobject 조회와 웹훅에 그대로 반환할 사용자 정의 값
recipient.externalRefstring 고객 식별자. 최대 255자이며 조회·웹훅에 반환
optionsobject 공유 정책. 생략 시 기본값 적용
messagestring 뷰어 안내 문구. 최대 1,000자이며 템플릿 변수 사용 가능

안내 문구 만들기

message에 아래 변수를 넣으면 공유 생성 시 실제 값으로 치환됩니다.

쓸 수 있는 값무엇이 들어가나
{{recipient.name}}수신자 이름
{{recipient.email}}수신자 이메일
{{recipient.phone}}수신자 전화번호
{{recipient.externalRef}}보내신 고객 식별자
{{sender.name}}발송 담당자 이름
{{sender.email}}발송 담당자 이메일

값이 없거나 지원하지 않는 변수는 invalid_message_template로 거절됩니다.

부가 정보 추가

위 세 요청 예시처럼 계약번호·지점 등 다시 받을 값은 최상위가 아닌 recipient.metadata에 넣습니다.

제한값이유
키 개수50개 조회와 웹훅에 반환
키 이름40자
값500자
키 문자A-Z a-z 0-9 _ -
값문자열만. 중첩 객체 불가
조회와 웹훅에 그대로 반환됩니다

검색용이 아니며 키 순서는 보장하지 않습니다. 제한 위반은 invalid_metadata입니다.

응답

201 응답의 id, links.recipient, links.host를 저장하세요.

{
  "id": "shr_fc1JjpkaDRRXwyKBG08uFg",        // 반드시 저장
  "links": {
    "recipient": "https://doc.throw.im/s/r/uO3xbWI4nQM8", // 고객에게 발송
    "host": "https://doc.throw.im/s/h/AaaaaAAAAA12", // 담당자 전용 · 저장 후 재사용
    "joinCode": "SFFCU" // 폐기 예정 — 사용하지 않음
  },
  "policy": { "expiresInDays": 90, "download": true, "watermark": false,
              "screenGuard": false, "personalNote": true, "branding": true },
  "adjustments": [],
  "recipient": { "name": "김민수", "externalRef": "cus_1001",
                  "metadata": null },
  "document": { "id": "doc_pJbchjs_mQ4QYKrkjTDkiA",   // 같은 파일을 또 보낼 때 재사용
                 "name": "계약서.pdf", "sizeBytes": 148231 },
  "request": {                                // 보낸 내용 그대로
    "options": { "expiresInDays": 90 },
    "message": "요청하신 계약서를 보내드립니다.",
    "recipient": { "name": "김민수", "email": "minsu@example.com",
                    "phone": null, "externalRef": "cus_1001", "metadata": null },
    "sender": null
  },
  "createdAt": "2026-08-27T00:00:00Z",
  "expiresAt": "2026-11-25T00:00:00Z"   // 링크가 죽는 시각
}
필드타입설명
idstring 조회·회수에 사용하는 공유 ID
links.recipientstring 고객에게 보낼 링크. GET /shares/{id}로 재조회 가능
links.hoststring 로그인 없이 입장하는 담당자 전용 주소. # 뒤까지 저장 후 반복 사용
links.joinCodestring 폐기 예정. 저장하거나 전달하지 말고 links.recipient를 사용하세요
policyobject 실제로 적용된 공유 정책
requestobject 서버가 인식한 요청 값
adjustmentsarray 요청과 실제 적용값의 차이
document.idstring 같은 파일을 다시 보낼 때 documentId로 재사용
expiresAtstring 링크 만료 시각(ISO 8601)
reference_not_stored 경고가 함께 온 경우

공유와 링크는 정상 생성됐지만 externalRef·metadata 저장에는 실패한 상태입니다. 응답의 id로 발송 기록을 연결하세요. 이때 recipient의 두 값은 null이고, 보낸 원본은 request.recipient에서 확인할 수 있습니다.

고객 링크와 담당자 링크

공유 생성 응답에 고객 링크와 담당자 링크가 함께 옵니다. 담당자 링크는 고객에게 보내지 말고 담당자 화면에 저장하세요.

구분고객 링크담당자 링크
사용자문서를 받는 고객문서를 보낸 담당자
처음 받기POST /shares 응답POST /shares 응답의 links.host
사용 기간공유 만료 전까지고객 링크 만료와 무관하게 유지
이후 처리저장 후 고객에게 전달저장 후 같은 주소로 재입장

호출 순서: 공유 생성 → id·links.recipient·links.host 저장 → 고객 발송 → 담당자는 저장한 주소로 입장

API 담당자 주소는 로그인 없이 열립니다. 일반 웹에서 만든 공유의 담당자 주소는 기존처럼 생성 계정 로그인이 필요합니다. 공유 회수·원본 삭제·세션 종료 후에는 열리지 않으며, 담당자 접근은 고객 열람·다운로드 횟수와 웹훅에 포함되지 않습니다.

이전에 발급된 #hostAccess=… 포함 주소도 계속 사용할 수 있습니다. 일반 웹 공유의 호스트 주소는 생성 계정 로그인이 필요합니다. API 생성 이력이 저장된 공유는 GET /shares/{id}로 짧은 호스트 주소를 조회할 수 있습니다.

실패 처리는 error.code 표를 기준으로 하고 기본 분기를 두세요.

GET/shares/{id}shares:read

공유 상태, 열람 기록, 고객·담당자 링크를 조회합니다.

응답

명시적으로 회수하면 links는 null, 없거나 다른 계정의 공유는 404입니다.

{ "id": "shr_fc1JjpkaDRRXwyKBG08uFg",
  "status": { "session": "WAITING",   // 아직 아무도 안 열었음
              "link": "active" },
  "links": { "recipient": "https://doc.throw.im/s/r/uO3xbWI4nQM8",
             "host": "https://doc.throw.im/s/h/AaaaaAAAAA12",
             "joinCode": "SFFCU" /* 폐기 예정 */ },
  "externalRef": "cus_1001",
  "expiresAt": "2026-11-25T00:00:00Z",
  "opened": { "count": 3, "lastAt": "2026-08-27T10:00:00Z" },
  "metadata": { "contractId": "c-99", "branch": "강남" },
  "downloads": { "count": 1 } }
필드타입설명
status.linkstring active · expired · revoked
status.sessionstring 뷰어 내부 상태: WAITING · OPEN · GRACE · CLOSED · EXPIRED
linksobject 고객·담당자 링크. 명시적 회수 시 null
externalRefstring 공유 생성 시 보낸 고객 식별자
metadataobject 공유 생성 시 보낸 값. 없으면 null
expiresAtstring 링크 만료 시각(ISO 8601)
opened.countinteger 문서 진입 횟수
downloads.countinteger 원본 다운로드 횟수
링크 복구

생성 응답을 놓쳤다면 고객 링크와 담당자 링크를 이 API로 복구하세요. 두 주소는 새로 발급되지 않고 처음 주소 그대로 반환됩니다.

DELETE/shares/{id}shares:write

공유의 모든 링크를 즉시, 되돌릴 수 없게 회수합니다.

응답

이후 조회의 links는 null입니다.

{ "ok": true, "revokedAt": "2026-08-27T00:00:00Z" }

발신자 정보

sender는 고객 화면에 표시할 담당자 정보입니다. 전체 위치와 형태는 위의 세 Shares 요청 예시에서 확인하세요.

생략하면 docs 계정 이름이 표시됩니다. email은 생략할 수 있습니다.

표시용 정보입니다

sender는 표시용입니다. 권한·감사 기록에는 요청에 사용한 토큰을 기준으로 삼으세요.

공유 옵션

모든 항목을 생략할 수 있으며 기본값이 적용됩니다.

필드타입 · 기본값설명
expiresInDaysinteger · 90*GET /me의 allowedExpiresInDays 중 하나. 기본값은 요금제 상한을 넘지 않음
personalNoteboolean · true열람 중 필기 허용
brandingboolean · true기본 브랜드 테마 표시
watermarkboolean · false열람자 워터마크 표시
allowDownloadboolean · true원본 다운로드 허용
screenGuardboolean · false화면 이탈 보호. 활성화하면 다운로드 차단

응답 형식

policy는 적용값, request는 인식한 요청, adjustments는 둘의 차이입니다.

요청·응답 필드 비교
보낼 때돌아올 때비고
options.allowDownloadpolicy.download adjustments[].field도 응답 쪽 이름(download)을 씁니다
options.expiresInDayspolicy.expiresInDays · expiresAt 이름은 같고 형태가 둘입니다
recipient.metadata생성 recipient.metadata · 조회 metadata 조회에서는 최상위로 올라옵니다
—POST /documents는 documentId · DELETE /documents/{id}는 id 같은 값인데 이름이 다릅니다
블록내용용도
links고객 링크 · 참여 코드고객에게 발송
policy실제로 강제되는 값고객 안내 문구
request보낸 내용 그대로확인·로깅
adjustments요청과 실제 적용이 다른 항목만비어 있으면 모두 반영된 것

자동 조정

아래 두 경우만 adjustments에 기록됩니다. 요금제 상한 초과는 403입니다.

reason언제결과
screen_guardscreenGuard: true + allowDownload: true다운로드가 강제 차단됨
no_default_themebranding: true + 계정에 기본 테마 없음브랜딩 미표시

request.options에는 서버가 인식한 키만 들어갑니다. 생략하면 null입니다.

오류 코드

message가 아닌 error.code로 분기하세요.

{ "error": {
    "code": "retention_exceeds_plan_limit",
    "message": "expiresInDays cannot exceed the current plan retention limit",
    "requestId": "req_8kZq3mVx7pLnR2wTy4",
    "details": { "requested": 365, "limit": 90 } } }
문의할 때는 requestId를 알려 주세요

X-Request-Id와 error.requestId를 로그에 남기세요. 직접 보낼 때는 영문·숫자·_ -, 64자 이내입니다.

인증·권한

상황오류 코드해결 방법
모든 호출이 403plan_upgrade_required 403공유 API는 비즈니스 플랜 전용입니다. details.currentPlan에서 현재 요금제를 확인하세요
모든 호출이 401invalid_token 401토큰 길이(52자)와 Bearer 접두사를 확인하세요
정상 호출이 401로 변경token_revoked 401토큰이 회수됐거나 90일간 사용되지 않았습니다. 새 토큰을 발급하세요
특정 API만 403missing_scope 403details.requiredScope에서 필요한 권한을 확인하세요

요청 내용

상황오류 코드해결 방법
공유 생성이 400idempotency_key_required 400Idempotency-Key 헤더를 추가하세요
recipient 누락recipient_required 400 공유마다 수신자를 입력하세요. null은 허용하지 않습니다
metadata 입력 후 400 invalid_metadata 400 키 50개·이름 40자·값 500자 제한입니다. details.key를 확인하세요
안내 문구 입력 후 400 invalid_message_template 400 지원하지 않는 변수이거나 값이 빠졌습니다. details를 확인하세요
이메일 입력 후 400invalid_email 400 이메일 형식을 확인하세요. 문제 필드는 details.field에 담깁니다
수신자 입력 후 400recipient_unreachable 400이메일이나 전화번호를 입력하세요. 이름만으로는 보낼 수 없습니다
유효기간 입력 후 400bad_request 400GET /me의 allowedExpiresInDays에서 값을 선택하세요
유효기간 입력 후 403retention_exceeds_plan_limit 403요금제 상한을 넘었습니다. maxExpiresInDays를 확인하세요
화면 이탈 보호 설정 후 403plan_feature_not_available 403비즈니스 요금제에서만 사용할 수 있습니다

파일·문서

상황오류 코드해결 방법
업로드가 413file_too_large 413GET /me의 maxUploadBytes를 확인하세요
업로드가 409storage_quota_exceeded 409details의 사용량과 한도를 확인한 뒤 문서를 정리하거나 요금제를 변경하세요
PDF 업로드 후 400unsupported_file_type 400PDF만 지원합니다. Content-Type도 확인하세요
file 필드 누락file_required 400multipart 필드명을 file로 지정하세요
documentId 조회가 404document_not_found 404문서가 없거나 다른 계정에 속해 있습니다

재시도·조회·서버

상황오류 코드해결 방법
재시도 후 409duplicate_request 409같은 키를 다른 내용으로 재사용했습니다. 새 요청에는 새 키를 사용하세요
동시 요청 후 409request_in_progress 409같은 키가 처리 중입니다. 잠시 후 다시 요청하세요
연속 요청 중 429rate_limited 429Retry-After만큼 기다리세요. 자세한 내용은 사용량 제한을 참고하세요
공유 조회가 404not_found 404공유를 생성할 때 사용한 담당자 토큰으로 요청하세요
서버 응답이 500 internal_error 500 requestId와 함께 고객 지원에 문의하세요

404는 없는 공유와 남의 공유를 구분하지 않습니다. 다른 계정의 공유 존재 여부는 알려주지 않습니다.

웹훅

고객이 문서를 열면 throw가 지정한 서버로 알림을 보냅니다. 폴링 대신 쓰세요.

이벤트언제
share.opened고객이 문서에 실제로 들어왔을 때 발송됩니다. 링크를 누르기만 해서는 발송되지 않습니다
share.downloaded원본을 내려받았을 때 발송됩니다
share.expired유효기간이 끝났을 때 발송됩니다
share.memo_created게스트가 메모를 만들었을 때 발송됩니다. 전체 공개 메모만 본문을 포함합니다
공유 상태별 웹훅 발송 지점 공유 생성 뒤 링크가 열려도 알림은 가지 않습니다. 고객이 실제로 들어오면 share.opened, 원본을 내려받으면 share.downloaded, 게스트가 메모를 만들면 share.memo_created, 유효기간이 끝나면 share.expired가 발송됩니다. 회수는 알림이 없습니다. 공유 생성 링크 열림 알림 없음 입장 share.opened 다운로드 share.downloaded 게스트 메모 share.memo_created 유효기간 끝 share.expired 회수 알림 없음

share.opened는 실제 문서 진입 시 발생하며 링크 클릭과 회수는 알리지 않습니다.

공유를 생성하는 계정마다 웹훅을 등록하세요

웹훅은 등록한 계정이 만든 공유에만 동작합니다. 같은 계정에서 발급한 여러 토큰은 웹훅 등록을 공유하므로 토큰마다 중복 등록하지 않습니다. 여러 계정으로 공유한다면 계정마다 등록하되, 수신 URL은 같아도 됩니다.

X-Throw-Delivery로 중복 알림을 걸러내세요

10초 안에 2xx로 응답하고 처리는 뒤에서 하세요. 이미 처리한 전송 ID는 버립니다.

POST /webhooks — 등록

필요 권한은 shares:read입니다. 등록한 계정이 만든 공유에서만 이벤트를 받습니다.

CRM 백엔드에서 초기 연동 때 한 번 호출하세요

수신 주소를 먼저 배포한 뒤 공유를 생성하는 각 계정에서 한 번 등록합니다. 같은 계정에서 발급한 여러 토큰은 기존 등록을 함께 사용하므로 공유할 때마다 다시 등록하지 마세요. 브라우저에서 호출하면 API 토큰이 노출됩니다. 기존 등록은 GET /webhooks로 확인할 수 있습니다.

curl -X POST $API/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "CRM 알림",
       "url": "https://crm.회사.com/hooks/throw",
       "events": ["share.opened", "share.downloaded", "share.expired", "share.memo_created"] }'

// 201 — 등록 완료
{ "id": "whk_Fh7daz1oQZmXvKr3tSNbLw",
  "name": "CRM 알림",
  "url": "https://crm.회사.com/hooks/throw",
  "events": ["share.opened", "share.downloaded", "share.expired", "share.memo_created"],
  "status": "active",
  "createdAt": "2026-08-27T09:00:00Z" }
필드필수설명
name필수웹훅 이름. 최대 100자
url필수알림을 받을 공개 HTTPS 주소. 최대 2,048자
events선택받을 이벤트 배열. 생략하면 네 이벤트 모두 등록
받을 이벤트 선택

events를 생략하면 네 이벤트를 모두 받습니다. 열람만 필요하면 ["share.opened"]를 보내세요.

id만 보관하면 설정은 끝납니다

상대방 CRM에는 수신 주소 외에 별도 인증값을 전달하지 않습니다.

URL 조건

공개 HTTPS 주소만 허용합니다. localhost·사설 IP·링크 로컬 주소는 거절됩니다.

잘못된 이름·URL·이벤트 또는 계정당 3개 제한 초과는 400 bad_request, 권한이 없으면 403 missing_scope입니다. 제한을 넘으면 error.details.limit가 반환됩니다.

담당자에게 알림 보내기

웹훅 데이터로 담당자를 찾은 뒤 사내 알림을 보내세요.

방법처리사용 시점
metadata { "staffId": "emp-042" } 저장 권장 — 내부 키 사용
sender.email 담당자 이메일로 조회 간단한 연동
externalRef 고객 ID로 역조회 현재 담당자에게 알림

웹훅 헤더

모든 알림에 아래 헤더가 포함됩니다.

헤더무엇
X-Throw-Event 본문 event와 같은 이벤트 이름
X-Throw-Delivery 중복 처리 방지용 전송 ID

이벤트별 요청 본문

네 이벤트의 공통 헤더 형식은 같습니다. X-Throw-Event와 본문의 event가 해당 이벤트 이름으로 바뀝니다.

POST https://crm.회사.com/hooks/throw
X-Throw-Event: share.opened
X-Throw-Delivery: dlv_90

share.opened — 고객이 문서에 실제로 들어왔을 때

{ "event": "share.opened",
  "webhookId": "whk_TAGr3xSZ2NpXeB4YSBqabQ",
  "createdAt": "2026-08-27T10:00:00Z",
  "data": {
    "links": {
      "host": "https://doc.throw.im/s/h/AaaaaAAAAA12xxxxxxxxxxxxxxxxxxxxxx",
      "recipient": "https://doc.throw.im/s/r/uO3xbWI4nQM8"
    },
    "shareId": "shr_1EuEW9BtUXqT4mLpKzR8Vw",
    "externalRef": "cus_9911",
    "metadata": { "contractId": "c-99", "branch": "강남" },
    "viewer": { "name": "김민수" },
    "sender": { "name": "박영업", "email": "yh.park@회사.com" }
  } }

share.memo_created — 게스트가 메모를 만들었을 때

{ "event": "share.memo_created",
  "webhookId": "whk_TAGr3xSZ2NpXeB4YSBqabQ",
  "createdAt": "2026-08-27T10:05:00Z",
  "data": {
    "shareId": "shr_1EuEW9BtUXqT4mLpKzR8Vw",
    "externalRef": "cus_9911",
    "metadata": { "contractId": "c-99" },
    "viewer": { "name": "김민수" },
    "sender": { "name": "박영업", "email": "yh.park@회사.com" },
    "memo": { "id": "memo_01", "page": 2, "visibility": "all", "text": "확인 부탁드립니다." }
  } }
메모 공개 범위에 따라 본문 포함 여부가 달라집니다

전체 공개(all)는 data.memo.text를 포함합니다. 나만 보기(private)와 선택 공개(selected)는 알림만 보내고 메모 본문은 보내지 않습니다.

share.downloaded — 원본 파일 다운로드 요청이 처리됐을 때

{ "event": "share.downloaded",
  "webhookId": "whk_TAGr3xSZ2NpXeB4YSBqabQ",
  "createdAt": "2026-08-27T10:03:00Z",
  "data": {
    "shareId": "shr_1EuEW9BtUXqT4mLpKzR8Vw",
    "externalRef": "cus_9911",
    "metadata": { "contractId": "c-99", "branch": "강남" },
    "viewer": { "name": "김민수" },
    "sender": { "name": "박영업", "email": "yh.park@회사.com" }
  } }

share.expired — 공유 유효기간이 끝났을 때

{ "event": "share.expired",
  "webhookId": "whk_TAGr3xSZ2NpXeB4YSBqabQ",
  "createdAt": "2026-11-25T00:00:00Z",
  "data": {
    "shareId": "shr_1EuEW9BtUXqT4mLpKzR8Vw",
    "externalRef": null,
    "metadata": null,
    "viewer": null,
    "sender": { "name": "박영업", "email": "yh.park@회사.com" }
  } }
필드타입설명
eventstring 이벤트 이름
webhookIdstring 등록된 웹훅 ID. 여러 등록을 구분할 때 사용
createdAtstring 이벤트 발생 시각(ISO 8601, UTC)
data.shareIdstring 공유 ID
data.externalRefstring · null 고객 링크의 열람·다운로드면 고객 식별자 반환. 만료 이벤트는 null
data.metadataobject · null 고객 링크의 열람·다운로드면 저장한 값 반환. 만료 이벤트는 null
data.viewerobject · null 열람자 표시 이름. 인증된 신원이 아니며 만료 이벤트는 null
data.senderobject · null 공유 생성 시 보낸 표시용 담당자
data.linksobject · null share.opened에만 포함. host는 담당자용, recipient는 입장한 고객의 링크(참가자 미확인 시 공개 게스트 링크). 사용할 수 없는 주소는 null. 기존 구독 그대로 받으며 호스트 주소는 CRM 내부에서만 보관하세요. 예시 입장권은 실제로 사용할 수 없습니다.
data.memoobject share.memo_created에서만 반환. 전체 공개일 때만 text 포함
externalRef의 유무로 고객 링크를 구분합니다

고객 링크로 열거나 다운로드하면 고객 식별자를, 다른 경로면 null을 반환합니다. share.expired는 특정 열람자 이벤트가 아니므로 externalRef·metadata·viewer가 모두 null입니다. 만료 건은 shareId로 저장한 발송 기록을 찾으세요.

수신 서버 예시

알림에 필요한 값만 확인해 받은 편지함에 저장한 뒤 응답합니다. 실제 채팅 알림은 작업 큐에서 보냅니다.

// 등록할 때 알려 준 그 주소
app.use(express.json())
app.post('/hooks/throw', async (req, res) => {
  const deliveryId = String(req.headers['x-throw-delivery'] || '')
  const payload = req.body
  // API 점검 도구의 단발 테스트. 업무 이벤트로 저장하지 않는다.
  if (payload?.event === 'webhook.test') return res.status(204).end()

  const allowed = ['share.opened', 'share.downloaded', 'share.expired', 'share.memo_created']
  if (!deliveryId || !allowed.includes(payload?.event) || !payload?.data?.shareId) {
    return res.status(400).end()
  }

  // 본문의 고객 정보는 그대로 믿지 않고, 저장해 둔 shareId로 발송 건을 찾는다.
  const shareRecord = await crmShares.findByThrowShareId(payload.data.shareId)
  if (!shareRecord) return res.status(200).end()

  // deliveryId에 UNIQUE를 걸어 같은 알림은 한 번만 저장한다.
  await webhookInbox.insertOnce({ deliveryId, event: payload.event, shareId: payload.data.shareId })
  res.status(200).end() // 10초 안에 응답
})

받은 편지함을 처리하는 작업자가 이벤트별 문구를 만든 뒤 사내 채팅 API를 호출하세요. 자동 처리의 근거가 필요하면 저장한 shareId로 GET /shares/{id}를 조회해 현재 상태를 확인하세요.

수신 서버 체크 사항

항목처리 방법
알려진 shareId 확인저장된 발송 건과 대조합니다
X-Throw-Delivery 저장재시도 중복을 차단합니다
빠르게 2xx받은 편지함에 저장한 뒤 10초 안에 응답하고 실제 처리는 큐로 넘깁니다

재시도

2xx가 아니면 4xx를 포함해 다시 보냅니다.

1회차 즉시 → 1분 → 5분 → 30분 → 2시간 → 6시간   (총 6회 · 약 9시간)
  • 연속 20회 실패하면 자동 비활성화됩니다. 해제 후 다시 등록하세요.
  • 상태 확인과 관리는 마이페이지 → 웹훅에서 할 수 있습니다.

조회·해제

아래 API도 Authorization: Bearer {토큰}과 shares:read 권한이 필요합니다.

엔드포인트권한용도
GET /webhooksshares:read목록·상태·연속 실패 횟수
GET /webhooks/{id}/deliveriesshares:read최근 실제 전달 20건의 헤더·본문·결과
POST /webhooks/{id}/testshares:read등록 주소로 단발 테스트 전송
DELETE /webhooks/{id}shares:read등록 해제

GET /webhooks 응답

[
  { "id": "whk_Fh7daz1oQZmXvKr3tSNbLw",
    "name": "CRM 알림",
    "url": "https://crm.회사.com/hooks/throw",
    "events": ["share.opened", "share.downloaded", "share.expired", "share.memo_created"],
    "status": "active",
    "consecutiveFailures": 0,
    "lastDeliveredAt": "2026-08-27T10:03:01Z",
    "createdAt": "2026-08-27T09:00:00Z" }
]

웹훅 전달 점검

API 점검 도구의 05 웹훅에서 인증용 토큰을 입력하고 열람·다운로드·만료·메모 작성 중 테스트할 이벤트를 선택하세요. 메타데이터(키/값 최대 3쌍)를 입력한 뒤 테스트 실행을 누르면 선택한 이벤트의 임의 데이터를 점검 도구의 수신 서버로 실제 전송합니다. 같은 화면에서 수신 확인된 헤더·JSON·수신 시각을 볼 수 있습니다. 별도 CRM 수신 주소는 필요하지 않습니다.

열람에는 data.links, 메모 작성에는 data.memo가 포함됩니다. 만료 이벤트는 실제 규격과 동일하게 metadata·externalRef·viewer가 null이며 입력한 메타데이터도 전달하지 않습니다.

문서·공유·열람 기록이나 임시 웹훅 등록은 만들지 않으며, 기존 CRM에도 보내지 않습니다. JSON의 data.metadata에서 입력값을 확인하세요. 고객·담당자 정보와 data.links는 임의 데이터이며 링크는 사용할 수 없습니다. 테스트 요청에는 X-Throw-Test: true가 포함됩니다. 수신 결과는 이 화면에서만 표시하며 입력 변경·새로고침 시 지워집니다. 기존 API 토큰 사용 기록은 갱신됩니다. CRM 수신 서버 자체를 점검하려면 아래 등록 주소 테스트를 사용하세요.

POST /webhooks/{id}/test — 요청 본문은 없습니다.

등록 주소로 아래 요청을 즉시 한 번 보냅니다. 공유나 업무 이벤트를 만들지 않고 테스트 전달 이력은 DB에 기록하지 않으며, 실패해도 재시도하지 않습니다. 기존 API 토큰 사용 기록은 갱신됩니다.

POST https://crm.회사.com/hooks/throw
Content-Type: application/json
X-Throw-Event: webhook.test
X-Throw-Delivery: dlv_test_Yt7dM2pQ

{ "event": "webhook.test",
  "webhookId": "whk_Fh7daz1oQZmXvKr3tSNbLw",
  "createdAt": "2026-09-07T03:00:00.000Z",
  "data": { "test": true } }

응답의 request에는 실제 보낸 주소·헤더·본문이, response에는 수신 서버의 상태 코드·처리 시간·오류가 들어갑니다. ok가 false여도 이 API 자체는 200으로 응답하므로 response를 확인하세요.

{ "deliveryId": "dlv_test_Yt7dM2pQ",
  "request": {
    "method": "POST",
    "url": "https://crm.회사.com/hooks/throw",
    "headers": {
      "Content-Type": "application/json",
      "X-Throw-Event": "webhook.test",
      "X-Throw-Delivery": "dlv_test_Yt7dM2pQ"
    },
    "body": { "event": "webhook.test", "webhookId": "whk_Fh7daz1oQZmXvKr3tSNbLw", "createdAt": "2026-09-07T03:00:00.000Z", "data": { "test": true } }
  },
  "response": { "ok": true, "statusCode": 204, "durationMs": 82, "error": null } }

GET /webhooks/{id}/deliveries?limit=20

실제 share.opened·share.downloaded·share.expired·share.memo_created 전달을 최신순으로 조회합니다. 각 항목에는 전송 헤더와 JSON 본문, 상태, 시도 횟수, 마지막 HTTP 응답과 실패 원인이 포함됩니다. 전송 대기 건은 전송 예정 내용이며, 수신 서버 내부 처리 완료 여부는 확인할 수 없습니다. limit은 1~100이며 기본값은 20입니다. 단발 테스트는 이 목록에 나타나지 않습니다.

DELETE /webhooks/{id} 응답 — 요청 본문은 없습니다.

{ "ok": true,
  "removedAt": "2026-08-27T11:00:00Z" }

형식이 잘못됐거나 없거나 다른 계정의 웹훅 ID는 모두 404 not_found로 반환합니다.

운영 참고사항

토큰

규칙내용
계정당 3개무중단 교체와 개발·운영 분리에 사용
만료 없음90일 미사용 시 자동 회수, 7일 전 앱 알림
원문은 한 번만throw는 해시만 저장합니다. 분실하면 새로 발급해 주세요
회수는 즉시마이페이지에서 회수하면 그 순간부터 모든 요청이 401입니다
호출 주소 기록마이페이지에서 IP별 호출 수 확인

무중단으로 토큰 바꾸기

① 새 토큰 발급        (이 시점에 활성 2개)
② 연동 시스템에 반영
③ 옛 토큰 회수        ← 순서를 지키면 끊기지 않습니다

현재 지원하지 않는 기능

기능지금은
공유 목록·검색24시간 안에는 같은 멱등 요청으로 응답을 재생할 수 있습니다. 그 이후를 위해 shr_…를 직접 저장하세요
여러 수신자에게 한 번에수신자마다 POST /shares를 따로 호출해 주세요
문서 목록 조회없습니다

연동 체크리스트

운영 배포 전에 확인하세요.

throw 공유 API · v1 OpenAPI 스펙은 문서 상단에서 내려받을 수 있습니다.