Hourwith API화면이 쓰는 그대로, 공개.

강사의 빈자리를 조회하고 예약을 넣는 공개 API. 인증 없음. 요청·응답은 JSON, 필드는 snake_case. 시각은 ISO 8601(UTC)이며 화면 표시는 한국 시간입니다. 결제는 손님이 강사 계좌로 직접 송금하고, Hourwith는 통신판매중개자로서 거래 당사자가 아닙니다.

오류는 모두 { "error": { "code", "message" } } 형태. message는 손님에게 그대로 보여줘도 되는 한국어 문장입니다.

GET/api/{slug}/slots?from=&to=

강사의 칸 목록. from, to는 2026-11-02(한국 날짜) 또는 ISO 8601 시각, 최대 62일. 생략하면 이번 주 월요일부터 2주.

classes는 강사가 쓴 수업 소개입니다. level: anyone | intro | beginner | intermediate | advanced | null(안 정함). photos의 첫 장이 대표 사진이고, 칸의 class_id로 이어집니다.

bookable이 true인 칸만 예약할 수 있습니다 (열림 · 자리 남음 · 시작 전). status: "closed"는 강사가 비공개로 돌린 칸입니다.

{
  "instructor": { "slug": "sujin", "name": "김수진 요가", "intro": "…", "instagram": "sujin.yoga" },
  "refund_rule": {
    "cancel_deadline_hours": 48,
    "late_refund_rate": 0,
    "text": "수업 48시간 전까지 취소하면 전액 환불, 이후엔 환불이 안 돼요."
  },
  "classes": [{
    "id": 1, "name": "빈야사", "category": "요가",
    "description": "호흡에 맞춰 동작을 이어 가는 60분…",
    "level": "beginner", "bring": "편한 옷",
    "photos": ["https://….public.blob.vercel-storage.com/classes/1/….jpg"]
  }],
  "from": "2026-11-01T15:00:00.000Z",
  "to": "2026-11-15T15:00:00.000Z",
  "slots": [{
    "id": 42,
    "starts_at": "2026-11-03T10:00:00.000Z",
    "ends_at": "2026-11-03T11:00:00.000Z",
    "class_id": 1, "class_name": "빈야사", "location": "성수",
    "price": 20000, "capacity": 6, "remaining": 2,
    "status": "open", "bookable": true
  }]
}

GET/api/explore?when=&tod=&hour=&cat=&area=&who=&sort=

모든 강사의 지금 예약 가능한 칸 (Hourwith 시간 찾기 화면 /explore와 같은 검색). 시간 찾기가 아직 열리지 않았으면 404 not_found. when: today | tomorrow | week(기본, 앞으로 7일) | weekend | 2026-11-03(7일 안). tod: early(~9시) | morning(9–12) | lunch(12–14) | afternoon(14–17) | evening(17–20) | night(20시~). hour는 시작 시각 한 시간(0–23, 한국 시간). cat·area는 정확히 일치, who는 강사 이름 일부. sort: soon(기본) | price | scarce(남은 자리 적은 순). 최대 100개, 전체 개수는 total.

{
  "from": "…", "to": "…", "total": 41,
  "slots": [{
    "id": 42, "starts_at": "…", "ends_at": "…",
    "class_name": "기구 필라테스", "category": "필라테스", "area": "합정",
    "location": "합정 민아 스튜디오", "price": 35000, "remaining": 1,
    "instructor": { "slug": "mina-pilates", "name": "박민아 필라테스" }
  }]
}

예약은 받은 instructor.slug와 id로 아래 POST를 부르면 됩니다.

POST/api/{slug}/bookings

예약 접수. 상태는 pending(입금 대기)으로 시작하고, 강사가 입금을 확인하면 confirmed. 손님은 pay_by(예약 후 강사가 정한 시간: 기본 30분, 1·2시간 또는 당일 자정. 수업이 더 일찍 시작하면 그 시각)까지 입금하고 /paid를 불러야 합니다. 지나면 예약은 cancel_reason: "expired"로 자동 취소되고 자리는 그 즉시 다른 손님에게 열립니다.

{ "slot_id": 42, "name": "홍길동", "phone": "010-1234-5678", "source": "api", "pay_method": "bank" }

source: link | story | explore | api (기본 api). explore는 Hourwith 탐색 화면에서 온 예약입니다. 성공하면 201과 아래 예약 객체. 강사에게 새 예약 문자가 갑니다. 손님에게는 접수 문자가 가지 않으니(문자는 확정·취소 때만),deposit의 계좌와 금액, pay_by, 예약자 이름으로 입금하라는 안내와 예약 페이지 /booking/{id} 링크를 꼭 보여주세요.

pay_method: bank(기본, 강사 계좌로 직접 계좌이체) | card(카드) | vbank(결제대행사 가상계좌로 계좌이체). Hourwith가 데려온 손님(source: "explore")은 결제대행사를 거쳐야 해서 card나 vbank만 되고, 그 밖의 예약은 bank만 됩니다. card·vbank는 pay_by까지 자리를 잡고, 결제는 브라우저에서 예약 페이지 /booking/{id}를 열어 합니다(결제창은 API로 열 수 없어요). 카드는 결제가 끝나면 바로 confirmed, 가상계좌는 계좌가 열리면 deposit에 나오고 입금되면 confirmed. 문자도 그때 갑니다. 이 예약들엔 /paid·/paid-late를 쓰지 않습니다.

400 invalid필드 누락·형식 오류 (휴대폰 번호 아님 등)
404 not_found없는 강사 또는 그 강사의 칸이 아님
409 closed비공개 칸
409 past이미 시작한 칸
409 full자리 없음
409 duplicate같은 번호로 이 칸에 이미 예약 있음
409 own_class로그인한 강사가 자기 수업을 예약하려 함
409 card_offexplore가 아닌 예약을 카드·가상계좌로 하려 함
409 bank_offexplore 예약을 강사 계좌 직접 입금(bank)으로 하려 함

GET/api/bookings/{id}

예약 상태. id는 예약 시 받은 UUID이고, 그 자체가 열쇠이므로 손님 이름·번호는 돌려주지 않습니다.

{
  "id": "6f1c…",
  "status": "pending",            // pending | confirmed | cancelled
  "amount": 20000,
  "refund_rule": { "cancel_deadline_hours": 48, "late_refund_rate": 0, "text": "…" },
                                  // 예약할 때의 환불 규칙. 강사가 나중에 규칙을 바꿔도 이 예약엔 이대로
  "slot": { "id": 42, "starts_at": "…", "class_name": "빈야사", "location": "성수" },
  "instructor": { "slug": "sujin", "name": "김수진 요가" },
  "pay_method": "bank",           // bank | card | vbank
  "deposit": { "bank_name": "국민", "account": "123-45-6789", "holder": "김수진", "amount": 20000 },
                                  // 송금할 곳: bank는 강사 계좌, vbank는 열린 가상계좌(holder null), card·계좌 전엔 null
  "receipt_url": null,            // 카드·가상계좌: 결제대행사 영수증 주소
  "created_at": "…",
  "pay_by": "…",                  // 이때까지 "입금했어요"가 없으면 자동 취소
  "paid_claimed_at": null,        // 손님이 "입금했어요"를 누른 시각 (누르면 pay_by 시계가 멈춤)
  "confirmed_at": null,
  "cancelled_at": null,
  "cancel_reason": null,          // instructor | expired
  "refund_amount": null,          // 취소 시 규칙대로 계산된 환불액 (bank는 강사가 송금, card는 결제 취소로 바로, vbank는 환불 계좌를 받으면 결제대행사가)
  "refund_account_at": null,      // 손님이 환불 계좌를 알려준 시각 (계좌는 강사에게만 보임)
  "refunded_at": null,            // 강사가 환불을 보냈다고 표시한 시각 (card·vbank는 결제대행사에 환불을 요청한 시각)
  "late_paid_claimed_at": null,   // 기한이 지난 뒤 "이미 입금했어요" 시각 (/paid-late)
  "revived_at": null              // 기한 후 입금을 강사가 되살려 확정한 시각
}

POST/api/bookings/{id}/paid

손님이 입금했다고 알림 ("입금했어요"). 강사에게 문자가 갑니다. 여러 번 불러도 문자는 처음 한 번만. 확정은 강사가 입금을 확인한 뒤에만 됩니다. pay_by가 지났으면 409 expired. 응답은 예약 객체.

POST/api/bookings/{id}/paid-late

기한이 지나 취소된 뒤 "이미 입금했어요" (입금은 했는데 /paid를 못 부른 경우). 한 번만 받고 강사에게 문자가 갑니다. 강사는 자리가 남아 있으면 되살려 확정하고(status: "confirmed", revived_at), 자리가 없으면 전액 환불(refund_amount = 금액, 이후 /refund-account), 입금이 없으면 refund_amount: 0. 기한이 지나지 않았거나 이미 알렸으면 409 conflict. 응답은 예약 객체(late_paid_claimed_at).

POST/api/bookings/{id}/refund-account

환불 받을 계좌. 강사가 환불액이 있는 예약을 취소하면 손님에게 이 링크가 문자로 갑니다. 강사가 환불을 보내기 전까지는 다시 불러 고칠 수 있고, 강사에게 문자는 처음 한 번만 갑니다. 환불할 게 없거나 이미 보냈으면 409 conflict. 응답은 예약 객체.

가상계좌(vbank)로 낸 돈은 결제대행사가 이 계좌로 바로 돌려줍니다(은행 영업일 2일, 한 번만). bank_name은 국민·신한·우리·하나·농협·기업·카카오뱅크·토스뱅크·케이뱅크·SC제일·우체국·새마을금고· 수협·iM뱅크(대구)·부산·경남·광주·전북·신협·씨티 중 하나여야 하고, 은행·계좌번호·예금주가 안 맞으면 409 bad_account.

{ "bank_name": "신한", "account": "110-123-456789", "holder": "홍길동" }

GET/api/bookings/{id}/messages

이 예약에 대해 손님과 강사가 주고받은 메시지, 오래된 것부터. 서로의 번호는 어디에도 없고 Hourwith가 전달합니다.

{ "messages": [{ "sender": "guest", "body": "10분 늦어요", "created_at": "2026-11-07T00:50:00.000Z" }] }

POST/api/bookings/{id}/messages

손님이 강사에게 남기는 메시지(1~500자). 강사에게 바로 알림이 가고, 강사가 답하면 손님에게 문자로 갑니다. 한 예약에 손님 메시지는 20개까지, 넘으면 409 too_many. 응답은 201과 위와 같은 목록.

{ "body": "10분 정도 늦을 것 같아요." }

POST/api/events

예약 창을 열었다는 기록(손님 화면이 navigator.sendBeacon으로 보냅니다). 그 칸의 가격·남은 자리와 함께 저장되고, 이름이나 번호는 받지 않습니다. 본문은 JSON 텍스트, 응답은 204(형식이 틀리면 400).

{ "type": "slot_view", "slot_id": 42, "source": "explore" }