양도 코드
양도 코드
판매자가 핀락에서 산 상품권을 PIN을 보지 않은 채 코드 하나로 업체(또는 거래자)에 넘기고, 업체는 그 코드를 확인·접수한 뒤 자기 구매자에게 핀락 열람 링크를 전달하고, 구매자는 핀락에서 PIN을 확인합니다. 중고 거래·재판매 마켓을 위한 기본 연동 방식입니다.
양도 코드란 #
핀락 고객이 상품권을 사면 주문 화면에 PLX-7K2M-Q9TB-4XWN 같은 양도 코드가 하나 보입니다. 고객(이하 판매자)은 PIN을 보지
않고 이 코드만 업체(재판매 마켓 등)에 넘깁니다. 업체는 API로 코드를 확인하고 접수(선점)한 뒤, 자기 서비스에서 결제한 구매자에게만
핀락 열람 링크를 만들어 전달합니다. 구매자가 핀락에서 링크를 열고 비밀번호를 넣어 PIN을 확인하면 거래가 끝납니다. 업체 서버는 PIN을 받지 않습니다.
- 판매자는 PIN을 보지 않습니다. 판매자 쪽에서는 PIN도, 전달 링크도 만들 수 없습니다. PIN을 먼저 보고 파는 일을 막습니다.
- 한 코드는 한 곳에서만 접수됩니다. 같은 코드를 여러 업체에 올려도 접수는 한 번만 성공합니다.
- 코드는 바뀌지 않습니다. 판매자는 코드를 새로 만들거나 바꿀 수 없어, 확인한 코드가 판매자 마음대로 무효가 되지 않습니다.
- 접수 중에는 판매자가 취소·환불을 할 수 없습니다. 업체가 매물을 올려 둔 사이 상품권이 사라지지 않습니다.
- 업체 서버는 PIN을 받지 않습니다. PIN은 구매자가 링크를 열 때 브라우저에만 한 번 표시됩니다.
전체 흐름 #
[판매자] 핀락에서 상품권 구매·입금 → 주문 화면의 양도 코드 복사
│ PLX-7K2M-Q9TB-4XWN (PIN은 보이지 않음)
▼
[업체 서버]
① GET /v1/transfers/code/{code} 코드 확인 (선점하지 않음, claimable 확인)
② POST /v1/transfers/claim 접수 요청 (코드 + 양도 비밀번호) → status: "pending_approval"
│
[판매자] 핀락 주문 화면에서 [승인] (시간 안에 승인하지 않으면 요청 자동 취소)
│
[업체 서버]
· GET /v1/transfers/{id} 로 status: "claimed_by_you" 확인 → 매물 등록
· 구매자가 업체에서 결제
③ POST /v1/transfers/{id}/share-link 구매자용 링크 + 비밀번호 생성
│
▼
[구매자] 링크를 열고 비밀번호 입력 → PIN 1회 확인
│
▼
[업체 서버]
④ GET /v1/transfers/{id} status: "delivered" 확인
⑤ 판매자에게 정산
거래가 깨지면: POST /v1/transfers/{id}/release → 링크 폐기, 판매자가 코드를 다시 쓸 수 있음
7일 안에 전달되지 않으면: 핀락이 자동 해제권장 연동 순서 #
판매자에게 코드 입력받기
매물 등록 화면에서 판매자에게 핀락 주문 화면의 양도 코드를 입력받습니다. 하이픈·공백·대소문자는 그대로 보내도 됩니다.
코드 확인하기 (선점하지 않음)
GET /v1/transfers/code/{code} 로 상태를 봅니다. claimable 이 true 인지, items·faceTotal 이
판매자가 올리려는 매물과 같은지 확인합니다. passwordSet 이 false 면 판매자에게 양도 비밀번호를 먼저 정해 달라고 안내합니다.
접수 요청하기
판매자에게 받은 양도 코드와 양도 비밀번호(숫자 6자리)로 POST /v1/transfers/claim 을 호출합니다. 성공(201)하면 status: "pending_approval" 이고, 받은 transferId 를 저장합니다. 비밀번호는 저장하지 마세요.
판매자 승인 기다리기
판매자에게 "핀락 주문 화면에서 등록을 승인해 주세요"라고 안내합니다. GET /v1/transfers/{transferId} 를 몇 초 간격으로 조회해 status 가 claimed_by_you 가 되면 매물을 공개합니다. approval.expiresAt 이 지나도록 승인되지 않으면 요청이 취소되어 조회가 404 가 됩니다.
구매자 결제 확인하기
구매자가 업체 서비스에서 결제를 마칠 때까지 기다립니다. 결제 전에는 링크를 만들지 마세요.
PIN 전달 링크 만들어 구매자에게만 보여 주기
POST /v1/transfers/{transferId}/share-link 로 링크를 만들고, 링크와 비밀번호를 구매자에게만 보여 줍니다. 비밀번호는 링크와 다른
곳(예: 구매 내역 화면과 문자)에 표시하는 것을 권장합니다.
전달 완료 확인 후 판매자 정산하기
GET /v1/transfers/{transferId} 를 주기적으로 조회해 status 가 delivered 가 되면 판매자에게 정산합니다.
거래가 취소되면 해제하기
구매자가 결제를 취소하는 등 거래가 깨지면 POST /v1/transfers/{transferId}/release 로 선점을 풉니다. 판매자는 코드를 다시 쓸 수 있게 됩니다.
코드 형식 #
PLX-뒤에 네 글자씩 세 묶음(영문 대문자·숫자, 헷갈리는0 O 1 I제외)입니다. 예:PLX-7K2M-Q9TB-4XWN- 하이픈 없이(
PLX7K2MQ9TB4XWN) 보내도 되고, 소문자로 보내도 같은 코드로 처리합니다. - 핀락 주문 하나에 코드는 하나이고, 판매자가 바꾸거나 다시 만들 수 없습니다. 그래서 한 번 확인한 코드는 접수·해제·전달이 끝나거나 접수 전에 판매자 주문이 취소·환불되지 않는 한 그대로 유효합니다. 판매자가 코드를 넘기고 대금을 받은 뒤 코드를 바꿔 무효로 만드는 일은 생기지 않습니다.
- 운영 키로는 실주문 코드만, 테스트 키로는 테스트 코드만 찾을 수 있습니다. 다른 모드의 코드는
404입니다.
transferId 와 codeLast4 만 저장하세요.1. 코드 확인 #
/v1/transfers/code/{code} 인증: API 키 코드의 현재 상태만 돌려주고 선점하지 않습니다. 몇 번을 호출해도 상태가 바뀌지 않으므로, 판매자가 코드를 입력할 때마다 호출해 매물 등록 가능 여부를
보여 주는 데 쓰세요. 다른 업체가 접수한 코드라면 claimed_by_other 만 알 수 있고 그 업체 정보는 나오지 않습니다.
curl https://api.pinlock.app/v1/transfers/code/PLX-7K2M-Q9TB-4XWN \
-H "Authorization: Bearer $PINLOCK_API_KEY"// 판매자가 입력한 코드. 하이픈·대소문자는 핀락이 알아서 맞춥니다
const code = input.transferCode.trim();
const res = await fetch(`https://api.pinlock.app/v1/transfers/code/${encodeURIComponent(code)}`, {
headers: { Authorization: `Bearer ${process.env.PINLOCK_API_KEY}` }
});
const body = await res.json();
if (!body.success) throw new Error(`${body.code}: ${body.error}`); // 404 NOT_FOUND 면 코드를 다시 확인해 달라고 안내
const transfer = body.data;
if (!transfer.claimable) {
// 이미 다른 곳에 접수됨, 입금 전, 전달 완료 등 — 매물로 받지 않습니다
return rejectListing(transfer.status);
}
// 상품·수량·액면가가 판매자가 올리려는 매물과 같은지 확인합니다
assertSameAsListing(transfer.items, transfer.faceTotal);import os, requests
from urllib.parse import quote
code = form["transfer_code"].strip() # 하이픈·대소문자는 핀락이 알아서 맞춥니다
res = requests.get(
f"https://api.pinlock.app/v1/transfers/code/{quote(code)}",
headers={"Authorization": f"Bearer {os.environ['PINLOCK_API_KEY']}"},
timeout=10,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['code']}: {body['error']}") # 404 NOT_FOUND 면 코드를 다시 확인해 달라고 안내
transfer = body["data"]
if not transfer["claimable"]:
# 이미 다른 곳에 접수됨, 입금 전, 전달 완료 등 — 매물로 받지 않습니다
reject_listing(transfer["status"])
# 상품·수량·액면가가 판매자가 올리려는 매물과 같은지 확인합니다
assert_same_as_listing(transfer["items"], transfer["faceTotal"]){
"success": true,
"data": {
"transferId": "trf_…",
"mode": "live",
"status": "available",
"claimable": true,
"items": [
{ "productName": "컬처랜드 문화상품권 1만원권", "faceValue": 10000, "quantity": 2 }
],
"faceTotal": 20000,
"paidAt": "2026-09-23T08:41:12.000Z",
"codeLast4": "4XWN"
}
}양도 객체 #
모든 양도 API 응답의 data 가 이 형식입니다(해제 제외). externalRef 부터 아래 필드는 그 코드를 접수한 업체에만
들어 있습니다.
| 필드 | 설명 |
|---|---|
transferId string | 양도 건 ID(trf_…). 링크 생성·조회·해제에 씁니다. 코드 원문 대신 이 값을 저장하세요. |
mode "live" | "test" | 실주문 코드인지 테스트 코드인지. 키 모드와 항상 같습니다. |
status string | 업체 입장에서 본 상태. 아래 상태 표 참고 |
claimable boolean | 지금 접수(선점)할 수 있는지. status 가 available 일 때만 true 입니다. false 면 매물로 받지 마세요. |
passwordSet boolean | 판매자가 양도 비밀번호를 정했는지. false 면 접수가 PASSWORD_NOT_SET 으로 거절되니, 판매자에게 먼저 정해 달라고 안내하세요. |
items[]. string | 상품 이름(판매자 주문 당시) |
items[]. number | 액면가(원) |
items[]. number | 수량(장) |
faceTotal number | 액면가 합계(원). 판매자가 핀락에 낸 금액이 아닙니다. |
paidAt string | null | 판매자의 핀락 입금이 확인된 시각. 입금 전이면 null |
codeLast4 string | 코드 끝 4자리. 판매자·운영자와 대조할 때 코드 원문 대신 쓰세요. |
externalRef string | null | 접수 때 보낸 업체 매물·거래 번호 (접수한 곳에만) |
claimedAt string | 접수(선점) 시각 (접수한 곳에만) |
approval. "pending" | "approved" | 판매자 승인 상태 (접수한 곳에만). pending 이면 판매자가 핀락에서 승인할 때까지 기다립니다. |
approval. string | null | 이때까지 판매자가 승인하지 않으면 접수 요청이 자동 취소되고 코드는 다시 available 이 됩니다. 기다리는 시간은 핀락 운영 설정을 따릅니다. |
approval. string | null | 판매자가 승인한 시각 |
claimExpiresAt string | 선점 만료 시각 = 승인 시각 + 7일. 이때까지 전달되지 않으면 자동 해제됩니다. (접수한 곳에만) |
deliveredAt string | null | 구매자가 링크를 열어 PIN을 확인한 시각 (접수한 곳에만) |
share object | null | 구매자용 PIN 전달 링크 정보. 링크를 만들기 전에는 null (접수한 곳에만) |
share. string | ready · opened · expired · locked · revoked · unavailable |
share. string | PIN 전달 링크. 링크 생성 응답에만 있습니다. |
share. string | 링크 만료 시각 = 링크를 만든 시각 + 24시간 |
share. string | null | 구매자가 PIN을 확인한 시각 |
share. number | 비밀번호를 틀린 횟수. 5회면 locked |
share. string | 현재 링크를 만든 시각(새로 만들면 바뀜) |
양도 상태 (status) #
| 상태 | 의미 |
|---|---|
available 접수 가능 | 판매자 입금이 확인되었고 아무 곳에도 접수되지 않았습니다. claimable 이 true 인 유일한 상태입니다. |
waiting_ 입금 대기 | 판매자가 핀락에 아직 입금하지 않았습니다. 접수할 수 없습니다(PAYMENT_PENDING). 입금 확인 뒤 다시 확인하세요. |
pending_ 판매자 승인 대기 | 이 업체가 접수를 요청했고 판매자의 승인을 기다립니다. 다른 곳은 접수할 수 없습니다. 링크는 아직 만들 수 없고(APPROVAL_PENDING), 해제는 할 수 있습니다. 시간 안에 승인되지 않으면 available 로 돌아갑니다. |
claimed_ 내가 접수함 | 판매자가 승인해 이 업체 앞으로 확정된 상태입니다. 링크 생성·해제를 할 수 있습니다. |
claimed_ 다른 곳에 접수됨 | 다른 업체가 이미 선점했습니다. 매물로 받지 마세요(ALREADY_CLAIMED). |
delivered 전달 완료 | 구매자가 링크를 열어 PIN을 확인했습니다. 끝난 상태입니다. |
unavailable 사용 불가 | 판매자 주문이 취소·환불·환불 심사·실패 등으로 양도할 수 없는 상태입니다. |
2. 접수 (선점) #
/v1/transfers/claim 인증: API 키 코드를 이 업체 앞으로 선점하고 판매자에게 승인을 요청합니다. 성공하면 201 과 함께 status: "pending_approval" 인 양도 객체가 옵니다. 이때부터 다른
곳은 이 코드를 접수할 수 없고, 판매자 주문 화면에 업체 이름·요청 시각·승인 버튼이 표시됩니다. 판매자가 승인하면 claimed_by_you 로 바뀌고, 승인 기한(approval.expiresAt)이
지나거나 판매자가 거절하면 접수가 취소됩니다.
| 필드 | 설명 |
|---|---|
code 필수 string | 판매자에게 받은 양도 코드. 하이픈 없이 보내도 되고 대소문자를 가리지 않습니다. 예) PLX-7K2M-Q9TB-4XWN |
password 필수 string (숫자 6자리) | 판매자가 핀락 주문 화면에서 한 번 정한 양도 비밀번호(바꿀 수 없음). 코드와 함께 판매자에게 받습니다. 5번 틀리면 그 코드는 60분 동안 접수할 수 없습니다. 저장하지 마세요. 예) 482913 |
externalRef 선택 string (최대 64자) | 업체 쪽 매물·거래 번호. 저장해 두었다가 조회 응답에 그대로 돌려줍니다. 개인정보는 넣지 마세요. 예) listing-58213 |
curl -X POST https://api.pinlock.app/v1/transfers/claim \
-H "Authorization: Bearer $PINLOCK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "PLX-7K2M-Q9TB-4XWN", "password": "482913", "externalRef": "listing-58213" }'const res = await fetch("https://api.pinlock.app/v1/transfers/claim", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PINLOCK_API_KEY}`,
"Content-Type": "application/json"
},
// password: 판매자가 정한 양도 비밀번호(6자리). 저장하지 말고 이 요청에만 씁니다
body: JSON.stringify({ code, password: sellerPassword, externalRef: listing.id })
});
const body = await res.json();
if (!body.success) {
// WRONG_PASSWORD(401) · LOCKED(423) · PASSWORD_NOT_SET → 판매자에게 비밀번호를 다시 확인
// ALREADY_CLAIMED · ALREADY_DELIVERED · PAYMENT_PENDING · UNAVAILABLE → 매물 등록을 막습니다
return rejectListing(body.code);
}
// 이 transferId 로 이후 링크 생성·조회·해제를 합니다. 코드 원문은 저장하지 않아도 됩니다
await db.saveTransfer(listing.id, body.data.transferId, body.data.claimExpiresAt);res = requests.post(
"https://api.pinlock.app/v1/transfers/claim",
headers={"Authorization": f"Bearer {os.environ['PINLOCK_API_KEY']}"},
# password: 판매자가 정한 양도 비밀번호(6자리). 저장하지 말고 이 요청에만 씁니다
json={"code": code, "password": seller_password, "externalRef": listing["id"]},
timeout=10,
)
body = res.json()
if not body["success"]:
# WRONG_PASSWORD(401) · LOCKED(423) · PASSWORD_NOT_SET → 판매자에게 비밀번호를 다시 확인
# ALREADY_CLAIMED · ALREADY_DELIVERED · PAYMENT_PENDING · UNAVAILABLE → 매물 등록을 막습니다
reject_listing(body["code"])
else:
# 이 transferId 로 이후 링크 생성·조회·해제를 합니다. 코드 원문은 저장하지 않아도 됩니다
db.save_transfer(listing["id"], body["data"]["transferId"], body["data"]["claimExpiresAt"]){
"success": true,
"data": {
"transferId": "trf_…",
"mode": "live",
"status": "pending_approval",
"claimable": false,
"passwordSet": true,
"items": [
{ "productName": "컬처랜드 문화상품권 1만원권", "faceValue": 10000, "quantity": 2 }
],
"faceTotal": 20000,
"paidAt": "2026-09-23T08:41:12.000Z",
"codeLast4": "4XWN",
"externalRef": "listing-58213",
"claimedAt": "2026-09-23T09:00:00.000Z",
"approval": {
"status": "pending",
"requestedAt": "2026-09-23T09:00:00.000Z",
"expiresAt": "2026-09-23T09:30:00.000Z",
"approvedAt": null
},
"claimExpiresAt": "2026-09-30T09:00:00.000Z",
"deliveredAt": null,
"share": null
}
}{
"success": false,
"code": "ALREADY_CLAIMED",
"error": "이미 다른 곳에 접수된 양도 코드입니다. 접수하지 마세요.",
"meta": { "status": "claimed_by_other" }
}- 양도 비밀번호가 필요합니다: 판매자가 핀락 주문 화면에서 한 번 정하는 숫자 6자리이며 바꿀 수 없습니다. 코드가 새어 나가도 비밀번호 없이는 접수할 수 없게
하려는 장치입니다. 5번 틀리면
423 LOCKED로 60분 동안 잠깁니다. 판매자는 틀린 횟수를 주문 화면에서 봅니다. - 판매자 승인이 필요합니다: 코드 주인이 "이 업체에 넘기는 게 맞다"고 확인하는 단계입니다. 코드와 비밀번호가 함께 새어 나가도 주인이 승인하지 않으면 양도가 확정되지 않습니다.
승인을 기다리는 시간은 핀락 운영 설정을 따르며(기본 30분), 응답의
approval.expiresAt으로 알 수 있습니다. - 다시 보내도 안전합니다: 이미 이 업체가 접수한 코드면 같은 양도 객체를 다시 돌려줍니다. 응답을 받지 못했다면 같은 코드로 다시 호출하세요.
- 같은 업체가 동시에 두 번 보내면 늦은 쪽은
409 ALREADY_CLAIMED를 받을 수 있습니다.409 ALREADY_CLAIMED를 받으면 코드 확인으로claimed_by_you인지 한 번 더 보고 처리하세요. - 실패 응답의
meta.status에 그 시점의 양도 상태가 들어 있습니다(동시 접수로 진 경우에는 없을 수 있습니다).
중복 등록 막기 #
판매자가 같은 코드를 여러 업체나 같은 업체의 여러 매물에 올리는 경우를 두 단계로 막습니다.
- 확인 단계 —
claimable: 코드 확인 응답의claimable이false면 매물 등록 화면으로 넘어가지 마세요. 상태별 안내 문구는 양도 상태를 참고하세요. - 등록 단계 — 원자적 접수: 확인과 접수 사이에 다른 곳이 먼저 접수할 수 있으므로, 확인 결과만 믿고 매물을 공개하면 안 됩니다. 접수는 핀락에서 한
번만 성공하도록 처리되어, 두 업체가 같은 순간에 접수해도 하나만
201을 받고 다른 하나는409 ALREADY_CLAIMED를 받습니다. 접수 응답이 성공일 때만 매물을 공개하세요.
같은 업체 안에서 같은 코드를 두 매물에 올리는 것도 막으려면, 접수 응답의 transferId 에 고유 제약을 두세요. 접수를 다시 호출하면 같은 transferId 가 오므로 이미 등록된 매물을 찾을 수 있습니다.
3. 양도 조회 #
/v1/transfers/{transferId} 인증: API 키 접수한 업체만 조회할 수 있습니다. 핀락은 전달 완료를 업체 서버로 먼저 알려 주지 않으므로, 링크를 만든 거래는 이 API로 30~60초 간격 조회해 delivered 를 확인하세요. 링크를 만들기 전인 거래는 자주 조회할 필요가 없습니다.
status: "delivered"— 구매자가 PIN을 확인했습니다. 판매자에게 정산하고 조회를 멈춥니다.share.status가expired·locked— 구매자가 열지 못했습니다. 새 링크를 만들어 다시 보여 주세요.404 NOT_FOUND— 해제했거나 7일이 지나 선점이 풀렸습니다. 거래를 멈추고 양쪽에 알립니다.
curl https://api.pinlock.app/v1/transfers/trf_여기에_ID \
-H "Authorization: Bearer $PINLOCK_API_KEY"// 링크를 만든 거래만 30~60초마다 확인합니다
for (const deal of await db.findDealsWaitingForDelivery()) {
const res = await fetch(`https://api.pinlock.app/v1/transfers/${deal.transferId}`, {
headers: { Authorization: `Bearer ${process.env.PINLOCK_API_KEY}` }
});
const body = await res.json();
if (res.status === 404) {
// 선점이 풀림(7일 자동 해제 또는 해제) — 거래를 멈추고 판매자·구매자에게 알립니다
await db.markDealReleased(deal.id);
continue;
}
if (!body.success) continue; // 일시 오류는 다음 주기에
const transfer = body.data;
if (transfer.status === "delivered") {
await db.markDelivered(deal.id, transfer.deliveredAt);
await settleToSeller(deal); // 구매자가 PIN을 확인했으니 판매자에게 정산
} else if (transfer.share && ["expired", "locked"].includes(transfer.share.status)) {
await askBuyerToRequestNewLink(deal); // 새 링크를 만들어 다시 보여 줍니다
}
}# 링크를 만든 거래만 30~60초마다 확인합니다
for deal in db.find_deals_waiting_for_delivery():
res = requests.get(
f"https://api.pinlock.app/v1/transfers/{deal['transfer_id']}",
headers={"Authorization": f"Bearer {os.environ['PINLOCK_API_KEY']}"},
timeout=10,
)
if res.status_code == 404:
# 선점이 풀림(7일 자동 해제 또는 해제) — 거래를 멈추고 판매자·구매자에게 알립니다
db.mark_deal_released(deal["id"])
continue
body = res.json()
if not body["success"]:
continue # 일시 오류는 다음 주기에
transfer = body["data"]
if transfer["status"] == "delivered":
db.mark_delivered(deal["id"], transfer["deliveredAt"])
settle_to_seller(deal) # 구매자가 PIN을 확인했으니 판매자에게 정산
elif transfer["share"] and transfer["share"]["status"] in ("expired", "locked"):
ask_buyer_to_request_new_link(deal) # 새 링크를 만들어 다시 보여 줍니다4. PIN 전달 링크 만들기 #
/v1/transfers/{transferId}/share-link 인증: API 키 구매자에게 보여 줄 PIN 전달 링크를 만듭니다. 판매자가 접수를 승인한 뒤(claimed_by_you)에만 만들 수 있습니다. 응답의 share.url 은 이 응답에서만 받을 수 있습니다. 판매자의 주문은 이미 입금이
확인되었으므로 링크는 만든 즉시 ready 이고, 만든 시각부터 24시간 동안 열 수 있습니다.
| 필드 | 설명 |
|---|---|
sharePassword 필수 string (6~20자) | 구매자가 링크를 열 때 입력할 비밀번호. 업체가 무작위로 만들어 구매자에게만 보여 주세요. 핀락은 해시로만 저장하므로 다시 조회할 수 없습니다. 예) 7K2m9Q |
curl -X POST https://api.pinlock.app/v1/transfers/trf_여기에_ID/share-link \
-H "Authorization: Bearer $PINLOCK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sharePassword": "7K2m9Q" }'import { randomInt } from "node:crypto";
// 구매자 결제가 확인된 뒤에만 호출합니다
const ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
const sharePassword = Array.from({ length: 8 }, () => ALPHABET[randomInt(ALPHABET.length)]).join("");
const res = await fetch(`https://api.pinlock.app/v1/transfers/${deal.transferId}/share-link`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PINLOCK_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ sharePassword })
});
const body = await res.json();
if (!body.success) throw new Error(`${body.code}: ${body.error}`);
// share.url 은 이 응답에서만 받을 수 있습니다. 암호화해 저장하고 구매자 화면에만 보여 줍니다
await db.saveBuyerLink(deal.id, encrypt(body.data.share.url), encrypt(sharePassword), body.data.share.expiresAt);import secrets, string
# 구매자 결제가 확인된 뒤에만 호출합니다
alphabet = string.ascii_letters + string.digits
share_password = "".join(secrets.choice(alphabet) for _ in range(8))
res = requests.post(
f"https://api.pinlock.app/v1/transfers/{deal['transfer_id']}/share-link",
headers={"Authorization": f"Bearer {os.environ['PINLOCK_API_KEY']}"},
json={"sharePassword": share_password},
timeout=10,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['code']}: {body['error']}")
# share.url 은 이 응답에서만 받을 수 있습니다. 암호화해 저장하고 구매자 화면에만 보여 줍니다
db.save_buyer_link(deal["id"], encrypt(body["data"]["share"]["url"]), encrypt(share_password), body["data"]["share"]["expiresAt"]){
"success": true,
"data": {
"transferId": "trf_…",
"mode": "live",
"status": "claimed_by_you",
"claimable": false,
"items": [{ "productName": "컬처랜드 문화상품권 1만원권", "faceValue": 10000, "quantity": 2 }],
"faceTotal": 20000,
"paidAt": "2026-09-23T08:41:12.000Z",
"codeLast4": "4XWN",
"externalRef": "listing-58213",
"claimedAt": "2026-09-23T09:00:00.000Z",
"claimExpiresAt": "2026-09-30T09:00:00.000Z",
"deliveredAt": null,
"share": {
"status": "ready",
"url": "https://…/share/…",
"expiresAt": "2026-09-24T11:20:05.000Z",
"openedAt": null,
"failedAttempts": 0,
"createdAt": "2026-09-23T11:20:05.000Z"
}
}
}링크 규칙 #
- 24시간: 링크를 만든 시각부터 24시간 동안 열 수 있습니다. 지나면
expired입니다. - 1회 열람: 구매자가 비밀번호를 맞게 입력하면 PIN이 한 번 표시되고 링크는
opened, 양도는delivered가 됩니다. - 5회 잠금: 비밀번호를 5번 틀리면
locked가 됩니다. - 새로 만들기: PIN이 열리기 전이면 언제든 다시 호출할 수 있습니다. 이전 링크는 바로 폐기되고 새 URL이 옵니다. 만료·잠금·유출 의심·링크 분실 때 쓰세요.
- 이미 전달된 건은
409 ALREADY_DELIVERED, 선점 기간이 지났거나 선점 중이 아니면409 CONFLICT입니다.
delivered 가 되어 되돌릴 수 없습니다.5. 선점 해제 #
/v1/transfers/{transferId}/release 인증: API 키 구매자 결제 취소, 매물 삭제 등으로 거래가 깨졌을 때 호출합니다. 요청 본문은 없습니다. 해제하면 그 코드로 만든 링크는 바로 폐기되고, 코드는 다시 available 이 되어 판매자가 다른 곳에 쓰거나 핀락에서 취소·환불할 수 있습니다. 해제한 뒤에는 이 transferId 로 조회할 수 없습니다(404).
curl -X POST https://api.pinlock.app/v1/transfers/trf_여기에_ID/release \
-H "Authorization: Bearer $PINLOCK_API_KEY"const res = await fetch(`https://api.pinlock.app/v1/transfers/${deal.transferId}/release`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.PINLOCK_API_KEY}` }
});
const body = await res.json();
if (!body.success) {
// 409 CONFLICT — 그 사이 구매자가 PIN을 열었을 수 있습니다. 조회해서 delivered 인지 확인합니다
throw new Error(`${body.code}: ${body.error}`);
}
await db.markDealReleased(deal.id); // body.data.status === "released"res = requests.post(
f"https://api.pinlock.app/v1/transfers/{deal['transfer_id']}/release",
headers={"Authorization": f"Bearer {os.environ['PINLOCK_API_KEY']}"},
timeout=10,
)
body = res.json()
if not body["success"]:
# 409 CONFLICT — 그 사이 구매자가 PIN을 열었을 수 있습니다. 조회해서 delivered 인지 확인합니다
raise RuntimeError(f"{body['code']}: {body['error']}")
db.mark_deal_released(deal["id"]) # body["data"]["status"] == "released"{
"success": true,
"data": { "transferId": "trf_…", "status": "released" }
}이미 PIN이 전달된 건은 해제할 수 없습니다(409 CONFLICT). 해제 직전에 구매자가 링크를 열었을 수 있으니, 409 를 받으면 조회로 확인하세요.
7일 자동 해제 #
접수 후 7일(claimExpiresAt) 안에 구매자가 PIN을 확인하지 않으면 핀락이 선점을 자동으로 풉니다. 해제와 똑같이 열리지 않은 링크는
폐기되고, 판매자는 코드를 다시 쓰거나 취소·환불을 요청할 수 있습니다.
- 매물 노출 기간을 7일보다 짧게 잡거나, 만료가 가까운 매물은 내리고 해제하세요.
- 링크는 선점이 풀리면 24시간이 남아 있어도 폐기됩니다. 구매자 결제가 확인되면 바로 링크를 만들어 주세요.
- 7일이 지나 풀린 코드를 다시 팔려면 판매자에게 코드를 다시 받아 처음부터(확인 → 접수) 진행합니다.
오류 #
분기는 항상 code 로 하세요. 공통 오류(401 UNAUTHORIZED 등)는 오류 코드에 있습니다.
| 상태 · code | 언제 · 대처 |
|---|---|
404 NOT_ | 코드 확인·접수 없는 코드(오타 등), 또는 다른 모드의 코드(운영 키에 테스트 코드 등). 판매자에게 코드를 다시 확인해 달라고 안내합니다. |
404 NOT_ | 조회·링크·해제 이 업체가 선점하지 않은 transferId, 또는 해제·자동 해제로 선점이 풀린 건. 거래를 멈춥니다. |
409 ALREADY_ | 접수 다른 곳에 이미 접수된 코드. 동시에 접수했을 때 늦은 쪽도 이 오류를 받습니다. 매물 등록을 막습니다. |
409 ALREADY_ | 접수·링크 생성 PIN 전달이 이미 끝났습니다. 접수하지 말고, 링크도 새로 만들 수 없습니다. |
409 PAYMENT_ | 접수 판매자의 핀락 입금이 아직 확인되지 않았습니다. 판매자에게 입금 확인 뒤 다시 등록하라고 안내합니다. |
409 UNAVAILABLE | 접수 판매자 주문이 취소·환불 등으로 양도할 수 없는 상태입니다. |
401 WRONG_ | 접수 양도 비밀번호가 틀림. meta.attemptsLeft 에 남은 횟수가 옵니다. 판매자에게 비밀번호를 다시 확인합니다. |
423 LOCKED | 접수 비밀번호를 5번 틀려 그 코드는 60분 동안 접수할 수 없습니다. 60분 뒤 판매자에게 비밀번호를 다시 확인해 시도합니다. |
409 APPROVAL_ | 링크 생성 판매자가 아직 접수를 승인하지 않았습니다. status 가 claimed_by_you 가 된 뒤 다시 만드세요. meta.approvalExpiresAt 에 승인 기한이 옵니다. |
409 PASSWORD_ | 접수 판매자가 아직 양도 비밀번호를 정하지 않았습니다. 판매자에게 핀락 주문 화면에서 정해 달라고 안내합니다. |
409 CONFLICT | 링크 생성·해제 선점 기간(7일)이 지남, 선점 중이 아님, 이미 전달되어 해제할 수 없음, 동시 처리 충돌. 조회해서 현재 상태를 확인합니다. |
422 VALIDATION_ | 전체 코드 형식(PLX-XXXX-XXXX-XXXX)이 아님, password 가 숫자 6자리가 아님, externalRef 64자 초과, sharePassword 6~20자 아님, transferId 형식 오류 |
403 FORBIDDEN | 테스트 코드 발급 운영 키로 POST /v1/test/transfers 를 호출함, 또는 업체 이용 중지 |
테스트 모드 #
/v1/test/transfers 인증: 테스트 키 전용 판매자가 코드를 넘겨준 상황을 만듭니다. 입금이 끝난 테스트 주문과 그 양도 코드를 만들어 transferCode 로 돌려줍니다. 이 코드로 확인 → 접수 → 링크 생성 →
링크 열기 → 해제까지 운영과 똑같이 시험할 수 있습니다. 본문의 items 는 [{ productId, quantity }] 형식이고, productId 는 GET /v1/products 로 조회합니다.
curl -X POST https://api.pinlock.app/v1/test/transfers \
-H "Authorization: Bearer $PINLOCK_TEST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "items": [{ "productId": "prd_여기에_상품_ID", "quantity": 1 }] }'const res = await fetch("https://api.pinlock.app/v1/test/transfers", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PINLOCK_TEST_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ items: [{ productId: "prd_여기에_상품_ID", quantity: 1 }] })
});
const { data } = await res.json();
console.log(data.transferCode); // "PLX-…" — 판매자가 넘겨준 코드처럼 쓰면 됩니다res = requests.post(
"https://api.pinlock.app/v1/test/transfers",
headers={"Authorization": f"Bearer {os.environ['PINLOCK_TEST_API_KEY']}"},
json={"items": [{"productId": "prd_여기에_상품_ID", "quantity": 1}]},
timeout=10,
)
data = res.json()["data"]
print(data["transferCode"], data["transferPassword"]) # 판매자가 넘겨준 코드·비밀번호처럼 쓰면 됩니다 (테스트 비밀번호는 항상 246813){
"success": true,
"data": { "transferCode": "PLX-3HRT-8QZC-M2VK", "transferPassword": "246813" }
}/v1/test/transfers/{transferId}/approve 인증: 테스트 키 전용 테스트 코드에는 승인해 줄 판매자가 없으므로, 접수(pending_approval) 뒤 이 API로 판매자가 승인한 것처럼 처리합니다. 응답은 status: "claimed_by_you" 인 양도 객체입니다.
호출하지 않으면 운영처럼 승인 기한이 지나 접수가 취소되는 흐름을 시험할 수 있습니다.
- 테스트 링크를 열고 비밀번호를 입력하면
TEST-xxxx-xxxx-xxxx형태의 무작위 테스트 PIN이 표시됩니다. 실제 상품권으로 쓸 수 없습니다. - 재고를 쓰지 않고 실제 판매자 주문도 만들지 않습니다. 24시간·1회 열람·5회 잠금·7일 자동 해제 규칙은 운영과 같습니다.
- 운영 키로 호출하면
403 FORBIDDEN입니다. 테스트 코드는 테스트 키로만, 실제 코드는 운영 키로만 찾을 수 있습니다. - 동시 접수를 시험하려면 코드 하나로 접수 요청 두 개를 동시에 보내 하나만
201인지 확인하세요.
보안 권장 사항 #
- 평문으로 기록하지 마세요: 양도 코드,
sharePassword,share.url은 요청·응답 로그와 오류 추적 도구에서 가려 주세요. - 코드는 가진 사람이 쓸 수 있는 값입니다: 매물 상세·검색 결과 등 다른 이용자가 보는 곳에 코드를 표시하지 마세요. 대조가 필요하면
codeLast4를 쓰세요. - 링크와 비밀번호는 구매자에게만, 서로 다른 곳에: 예를 들어 링크는 구매 내역 화면, 비밀번호는 문자로 보여 주세요.
- 비밀번호는 업체가 무작위로 만들고(8자 이상 권장), 꼭 보관해야 한다면 암호화해 두세요.
- 유출이 의심되면 바로 새 링크를 만들어 이전 링크를 폐기하세요.