# LinkFarm Brand API — 전체 통합 가이드 Base URL: `https://openapi.linkfarm.ai` API 버전: `v1` (URL 에 고정. 깨는 변경은 `/v2` 로만 갑니다) ## 인증 `Authorization: Bearer ` - `sk_live_…` / `sk_test_…` — **서버 전용**. 주문·환불·취소·확정 전부 가능. - `pk_live_…` / `pk_test_…` — 브라우저용 publishable. 페이지뷰·장바구니· `order.observed` 까지만. 공개 키로 정산 대상 주문을 만들 수 없습니다 (누구나 매출을 위조할 수 있게 되므로). `sk_test_` 키로 보낸 이벤트는 `livemode:false` 로 표시됩니다. HMAC 서명은 필요 없습니다. TLS + Bearer 로 충분합니다. 키는 브랜드 인증 API에서 발급합니다. 브랜드 로그인 JWT로 `POST https://api.linkfarm.ai/api/v2/biz/integrations/{integration_id}/api-keys` 에 `{"livemode":false}` 또는 `{"livemode":true}`를 보내세요. secret 평문은 발급 응답에서 한 번만 보이며, 재발급하면 같은 모드의 이전 키는 즉시 폐기됩니다. ## 첫 호출 ```bash curl -s https://openapi.linkfarm.ai/v1/ping -H "Authorization: Bearer $LINKFARM_SECRET_KEY" ``` → `{"ok":true,"brand":"...","livemode":false,"key_type":"secret"}` ## 클릭 귀속 크리에이터 링크로 유입되면 랜딩 URL 에 `?lf_click=b_xxxx` 가 붙습니다. **서버에서 읽어 자사 도메인 쿠키로 저장하세요**(HttpOnly, 13개월 권장). 브라우저 JS 로 굽는 쿠키는 Safari ITP 규칙 때문에 **24시간으로 잘립니다** (트래커가 보낸 페이지 + 쿼리스트링 조합). 서버 `Set-Cookie` 는 이 제한을 받지 않습니다. 결제 시 그 값을 `data.click_ref` 로 보내면 됩니다. 크리에이터 할인코드를 쓰는 경우 `data.coupon_code` 를 보내고, **둘 다 있으면 코드가 우선**합니다. ### 경량 스크립트(lf.js) — 클릭 캡처를 대신 해주는 태그 서버에서 쿠키를 심기 어려우면 전 페이지 공통 레이아웃에 두 줄을 넣으세요 (첫 줄 스텁이 스크립트 로딩 전의 `lf()` 호출을 큐에 보관합니다): ```html ``` - `lf_click` 을 자동으로 자사 도메인 쿠키(`_lf_click`)+localStorage 에 저장 합니다. 결제 시 서버에서 그 쿠키를 읽어 `data.click_ref` 로 보내면 됩니다. - `data-pk` 를 넣으면 유입 신호(`page.viewed`)가 자동 전송되고, 주문완료 페이지에서 `lf('order', {order_id:'...', amount:59000})` 를 호출하면 `order.observed` 관측 이벤트가 전송됩니다 — 서버 전송이 누락된 주문을 찾아내는 대사 신호로 쓰입니다. - **정산은 여전히 secret key 서버 전송으로만 확정됩니다.** 스크립트는 클릭 캡처와 관측용이며, JS 쿠키의 Safari ITP 제한(위)도 그대로 받습니다. 클릭 인정기간(제품 설정, 기본 30일)은 **해당 오퍼의 가장 최근 클릭** 기준으로 서버가 판정합니다 — 구매자 개인의 클릭 시각 단위가 아닙니다. `click_ref` 는 결제 완료 시점까지 보관했다가 그대로 보내주세요. ## 이벤트 전송 ``` POST /v1/events Content-Type: application/json Authorization: Bearer sk_live_… ``` ```json { "type": "order.paid", "occurred_at": "2026-08-01T14:32:18+09:00", "data": { "order_id": "20260801-000123", "amount": 59000, "currency": "KRW", "click_ref": "b_8mK3qP", "coupon_code": "SUJIN10", "items": [ {"sku": "TSHIRT-M", "name": "화이트 티셔츠", "quantity": 1, "amount": 59000} ] } } ``` ### `amount` 정의 (중요) **할인 적용 후 소비자가 실제 부담한 커미션 대상 상품 금액.** 배송비·포인트 충전·상품권·비대상 상품은 제외합니다. 한 주문에 제휴 대상과 비대상 상품이 섞이면 대상분만 보내세요 — 전체 결제액을 보내면 비대상 상품에까지 커미션이 붙어 과다청구됩니다. `amount` 는 `currency` 의 최소단위입니다(KRW=원, USD=센트). KRW 가 아니면 `fx_rate`(1 통화단위당 KRW)가 필수이고, 그 환율이 주문에 고정되어 **환불도 같은 환율로 계산**됩니다. ### `occurred_at` 이벤트가 실제 발생한 시각(선택, 생략 시 수신 시각). **UTC 오프셋 필수** — `2026-08-01T14:32:18+09:00` 처럼 보내세요. 오프셋 없는 값은 `invalid_datetime`(422)으로 거절됩니다. 허용 범위는 현재 시각 기준 **과거 90일 ~ 미래 5분**이며 벗어나면 `occurred_at_out_of_range`(422)입니다. ### 이벤트 타입 secret key 전용: `order.paid`, `refund.created`, `order.cancelled`, `order.confirmed` publishable 허용: `page.viewed`, `product.viewed`, `cart.item_added`, `order.observed` ### data 필드 - `order_id` — 쇼핑몰의 주문번호. 브랜드 안에서 유일해야 합니다(멱등 키). - `amount` — 할인 적용 후 소비자가 실제 부담한 **커미션 대상** 상품 금액. 배송비·포인트 충전·상품권·비대상 상품 제외. 한 주문에 제휴 대상과 비대상이 섞이면 대상분만 보내세요. currency 의 최소단위(KRW=원, USD=센트). - `currency` — ISO 4217. 생략하면 KRW. 지원: KRW·USD·JPY·EUR·GBP. - `fx_rate` — 1 통화단위당 KRW 환율. KRW 가 아니면 필수. 주문에 고정되어 이후 환불도 같은 환율로 계산됩니다. - `click_ref` — 랜딩 URL 의 `lf_click` 값. 자사 도메인 쿠키에 저장했다가 보냅니다. - `coupon_code` — 크리에이터 할인코드. click_ref 와 둘 다 있으면 **코드가 우선**합니다. - `parent_order_id` — 구독 갱신 결제일 때 최초 결제의 order_id. 귀속을 상속하고 회차가 자동 계산됩니다. - `refund_id` — 환불 식별자. 한 주문에 환불이 여러 번 들어올 수 있어 필요하고, 같은 값 재전송은 멱등입니다. - `reason` — 환불·취소 사유(자유 텍스트). - `last_touch_channel` — 마지막 유입 채널. 어필리에이트가 last-click 이 아니면 다른 값을 보내 중복 계상을 피할 수 있습니다(Awin `ch` 상당). - `customer_hash` — 구매자 식별자의 **16진 해시**(sha256 권장, 32~64자). 자기구매 판정에만 씁니다. **원문 PII 는 받지 않습니다** — 이메일·전화·이름을 넣으면 422 로 거절됩니다. - `items` — 주문 라인아이템(선택). 상품별 분석에 쓰입니다. ## 환불 · 취소 ```json {"type": "refund.created", "data": {"order_id": "20260801-000123", "refund_id": "R2", "amount": 19000}} ``` 한 주문에 환불이 **여러 번** 들어올 수 있습니다. `refund_id` 로 각각을 구분하며 같은 `refund_id` 재전송은 멱등입니다. 부분환불은 주문을 종결시키지 않고, 누계가 원금에 도달할 때 종결됩니다. 전액취소는 금액을 생략합니다: ```json {"type": "order.cancelled", "data": {"order_id": "20260801-000123"}} ``` ## 구매확정 ```json {"type": "order.confirmed", "data": {"order_id": "20260801-000123"}} ``` 원천몰 상태 확인용으로 기록되며 **지급 일정을 앞당기지 않습니다**. ## 구독 갱신 갱신 결제는 클릭도 코드도 없으므로 최초 결제의 주문번호를 실어 보내세요: ```json {"type": "order.paid", "data": {"order_id": "20260901-000999", "amount": 14900, "parent_order_id": "20260801-000123"}} ``` 최초 결제의 귀속을 상속하고 회차가 자동으로 계산됩니다. ## 멱등 · 누락 복구 `order.paid` 는 `order_id` 기준 **upsert** 입니다. 같은 주문을 다시 보내면 원장이 두 번 생기지 않고 **현재 값으로 갱신**됩니다 — 금액 정정, 늦게 확보한 쿠폰/클릭, 환율 정정이 반영됩니다. 그래서 **누락 복구가 간단합니다**: > 매일 밤 어제 주문 전량을 `POST /v1/events/batch` (최대 500건)로 재전송하세요. 배치는 순서대로 개별 반영됩니다. 중간에 실패하면 앞선 이벤트는 반영된 채 에러가 반환되고, `error.param` 이 `events[137].data.amount` 처럼 **실패한 이벤트의 인덱스**를 가리킵니다. 멱등이므로 배치를 통째로 재전송해도 안전합니다. 레이트리밋은 요청 수가 아니라 **이벤트 수** 기준으로 계량됩니다. ⚠️ **지급이 끝난 주문은 더 이상 바뀌지 않습니다.** 재전송해도 에러는 아니지만 (배치가 통째로 실패하면 안 되므로) 금액은 그대로 유지됩니다. 우리가 인식한 상태는 `GET /v1/orders` 로 언제든 대조할 수 있습니다. 브랜드가 조회용 API 를 따로 호스팅할 필요는 없습니다. 500건이 넘으면 커서로 이어집니다: ``` GET /v1/orders?limit=500 → {"data": [...], "has_more": true, "next_cursor": "eyJ..."} GET /v1/orders?limit=500&cursor=eyJ... ``` ## 에러 ```json {"error": { "type": "invalid_request_error", "code": "refund_exceeds_remaining_amount", "message": "환불 누계가 원주문 금액을 초과합니다.", "param": "data.amount", "hint": "주문 'X' 의 남은 환불 가능 금액은 40,000원입니다.", "doc_url": "https://linkfarm.ai/developers/errors#refund_exceeds_remaining_amount", "request_id": "req_...", "retryable": false }} ``` `retryable:true` (429·5xx)만 지수 백오프로 재시도하세요. 4xx 는 재시도하지 마세요. 문의 시 `request_id` 를 알려주시면 바로 추적됩니다. ## 진단 - `GET /v1/ping` — 키·브랜드·모드 - `GET /v1/orders/{order_id}` — 우리가 인식한 주문 상태(귀속 여부·커미션·환불 누계) - `GET /v1/orders` — 대사용 목록 - `GET /v1/integration/status` — 최근 24시간 클릭·주문·미귀속 수 정상 수신했지만 쿠폰/클릭을 찾지 못한 주문도 조회에서 빠지지 않고 `status:"unattributed"`, `attributed:false`로 반환됩니다. 이 주문을 쿠폰 또는 `click_ref`와 함께 재전송하면 같은 주문번호에 귀속이 보강됩니다. ## 개인정보 구매자 이름·이메일·전화·주소는 **보내지 마세요.** 자기구매 판정이 필요하면 `data.customer_hash` 에 해시만 보내면 됩니다.