MODOO 외부 연동 API v1

MODOO 밖의 프로그램(예: CP DIRECT 택배 예약 자동화)이 한 회사의 고객·거래를 읽고, 고객에게 메모를 남기는 데 쓰는 API 다.

  • 이 문서: https://modoo.com.au/docs/api
  • 기본 주소: https://modoo.com.au/api/v1
  • 요청·응답 모두 JSON(UTF-8). 모든 응답에 Cache-Control: no-store 가 붙는다.
  • 시각(createdAt 등)은 ISO 8601 UTC 문자열, 날짜(contractDate 등)는 yyyy-MM-dd 문자열이다.
  • 값이 없으면 필드를 빼지 않고 null(문자열 칸은 경우에 따라 "")로 준다. 응답 모양이 항상 같다.

1. 인증

모든 요청에 API 키를 Bearer 토큰으로 넣는다.

Authorization: Bearer modoo_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

키는 modoo_ 뒤에 43 글자가 붙은 문자열이다. 키가 없거나, 형식이 틀리거나, 없는 키이거나, 폐기된 키이면 이유와 상관없이 똑같이 401 unauthorized 를 준다.

키 발급 (회사 관리자)

키는 MODOO 앱 안에서 회사 관리자(대표·관리자 역할)가 직접 만든다. 운영자에게 따로 요청할 필요가 없다.

  1. MODOO 에 관리자 계정으로 로그인한다.
  2. 오른쪽 위 프로필 사진 → 프로필 로 들어간다.
  3. 아래쪽 API 키 카드에서 키 만들기 를 누른다. (관리자가 아니면 이 카드가 보이지 않는다.)
  4. 어디에 쓰는 키인지 알아볼 수 있게 이름을 적는다(1–60자). 예: CP DIRECT 택배 예약
  5. 만들어진 키가 한 번만 보인다. 복사 버튼으로 복사해 연동 프로그램의 비밀 설정에 넣는다. 대화상자를 닫으면 MODOO 도 다시 보여 줄 수 없다(서버에는 키의 해시만 저장한다). 잃어버렸으면 폐기하고 새로 만든다.
  • 회사당 사용 중인 키는 최대 10 개다. 넘기면 만들 수 없으니 쓰지 않는 키를 폐기한다.
  • 목록에는 이름, 짧은 ID, 만든 시각, 마지막 사용 시각(최대 1시간 단위로 갱신), 상태(사용 중/폐기됨)가 보인다. 키 원문은 목록에 나오지 않는다.
  • 연동마다 키를 따로 만들어 두면, 하나가 새어 나가도 그것만 끊을 수 있다.

키 폐기

같은 API 키 카드의 목록에서 해당 키의 폐기 를 누르고 확인한다. 즉시 효력이 생기며 되돌릴 수 없다. 폐기된 키는 기록으로 목록에 남는다.

키가 멈추는 경우

  • 폐기했을 때.
  • 키를 발급한 관리자가 퇴사 처리되었거나, 다른 회사로 옮겨졌거나, 직원으로 강등되었을 때(대표·관리자 역할이 아니게 되었을 때). 역할은 요청마다 새로 확인한다. 이 키로 남긴 메모의 작성자가 그 관리자로 기록되고, 키를 만들 수 없는 사람의 키가 회사 고객 전체를 계속 내보내면 안 되기 때문이다. 담당 관리자가 바뀌면 새 관리자가 키를 새로 만들어 교체한다.

회사 범위

  • 키는 만든 관리자의 회사에 묶인다. 요청으로 회사를 지정하는 방법은 없고, 어떤 파라미터로도 다른 회사 데이터를 볼 수 없다.
  • 다른 회사 고객의 id 로 요청하면 403 이 아니라 404 not_found 를 준다. 그 id 가 어딘가에 존재한다는 사실도 알려 주지 않는다.

2. 오류

오류 응답은 항상 이 모양이다.

{ "error": { "code": "bad_request", "message": "limit must be an integer between 1 and 200." } }
HTTPcode뜻
400bad_request파라미터·본문이 잘못됨. message 에 이유가 적힌다.
401unauthorized키가 없거나 쓸 수 없음. WWW-Authenticate: Bearer 헤더가 함께 온다.
404not_found고객이 없거나 다른 회사 고객.
413payload_too_large요청 본문이 16 KB 를 넘음.
429rate_limited요청 한도 초과. Retry-After 헤더의 초만큼 기다린 뒤 다시 보낸다.
500internal서버 오류. 잠시 뒤 다시 시도한다.

요청 한도

  • 키마다 분당 60 회. 한꺼번에 60 회까지 몰아 쓸 수 있고, 이후 1 초에 1 회씩 다시 찬다. 동시에 몰아 보낸 요청도 도착 순서대로 세어 60 회를 넘는 것은 429 를 받는다.
  • 인증에 실패한 요청(키 없음·틀린 키·폐기된 키)에는 따로 한도를 두지 않고 401 을 준다. 틀린 키로 반복 호출하지 않도록 한다.
  • 429 를 받으면 Retry-After(초) 만큼 기다린 뒤 다시 보낸다.
  • 고객·거래 목록(/customers, /deals)은 서버가 회사 고객을 최대 30 초 들고 있다가 페이지 넘김에 다시 쓴다. 앱에서 방금 고친 고객이 목록에 최대 30 초 늦게 보일 수 있다. 고객 상세의 메모와 메모 쓰기는 항상 최신이다.

3. 엔드포인트

3.1 GET /api/v1/me — 키 확인

키가 동작하는지, 어느 회사의 키인지 확인한다. 연동을 설정한 직후 가장 먼저 부르면 된다.

curl -s https://modoo.com.au/api/v1/me \
  -H "Authorization: Bearer $MODOO_API_KEY"
{
  "data": {
    "companyId": "3kd9Q2x…",
    "companyName": "예시 상사",
    "keyName": "CP DIRECT 택배 예약"
  }
}

3.2 GET /api/v1/customers — 고객 목록

앱의 고객 화면은 상태가 Won 인 고객만 보여 주지만, 이 API 는 모든 상태(리드 포함)를 다루고 status 로 거른다.

파라미터기본값설명
q없음이름·회사·이메일·주소·연락처에서 대소문자 구분 없이 부분 일치로 찾는다. 검색어가 숫자와 공백·-·(·)·+·. 로만 되어 있으면 연락처는 숫자만 남겨 비교한다. 0491 570 156 로 찾아도 0491570156 이 걸린다.
status전체쉼표로 구분한 상태. New Lead, Contacted, Proposal, Won, Lost 중에서. 대소문자까지 정확히 적는다.
limit501–200. 범위를 벗어나면 400.
offset00 이상 정수.

정렬: 등록 시각(createdAt) 최신순, 같으면 id 순. 등록 시각이 없는 고객은 맨 뒤.

curl -s -G https://modoo.com.au/api/v1/customers \
  -H "Authorization: Bearer $MODOO_API_KEY" \
  --data-urlencode "status=Won" \
  --data-urlencode "q=0491 570 156" \
  --data-urlencode "limit=20"
{
  "data": [
    {
      "id": "Lx82hQ…",
      "name": "Sumin Lee",
      "company": "",
      "contact": "0491 570 156",
      "email": "sumin@example.com",
      "address": "Unit 3, 12 Smith St, Sydney NSW 2000",
      "addressParts": {
        "streetNumber": "Unit 3/12",
        "streetName": "Smith St",
        "suburb": "Sydney",
        "state": "NSW",
        "postcode": "2000"
      },
      "status": "Won",
      "temperature": "Hot",
      "createdAt": "2026-03-01T04:12:55.120Z",
      "statusUpdatedAt": "2026-03-05T01:00:00.000Z",
      "dealCount": 1
    }
  ],
  "total": 1,
  "limit": 20,
  "offset": 0
}

total 은 조건에 맞는 전체 건수다. 다음 페이지는 offset 에 limit 을 더해 부르고, offset >= total 이면 끝이다.

3.3 GET /api/v1/customers/{customerId} — 고객 상세

고객 정보에 거래(deals)와 메모(notes)를 붙여 준다.

curl -s https://modoo.com.au/api/v1/customers/Lx82hQ… \
  -H "Authorization: Bearer $MODOO_API_KEY"
{
  "data": {
    "id": "Lx82hQ…",
    "name": "Sumin Lee",
    "company": "",
    "contact": "0491 570 156",
    "email": "sumin@example.com",
    "address": "Unit 3, 12 Smith St, Sydney NSW 2000",
    "addressParts": { "streetNumber": "Unit 3/12", "streetName": "Smith St", "suburb": "Sydney", "state": "NSW", "postcode": "2000" },
    "status": "Won",
    "temperature": "Hot",
    "createdAt": "2026-03-01T04:12:55.120Z",
    "statusUpdatedAt": "2026-03-05T01:00:00.000Z",
    "dealCount": 1,
    "deals": [
      {
        "id": "7f1c…",
        "customerId": "Lx82hQ…",
        "contractDate": "2026-09-12",
        "salesType": "판매",
        "contractAmount": 1290,
        "items": [{ "productName": "Water Purifier A1", "quantity": 1, "unitPrice": 1290 }],
        "oneTimeFee": null,
        "paymentType": null,
        "isIndefinite": null,
        "rentalStartDate": null,
        "rentalEndDate": null,
        "rentalPeriod": null,
        "rentalPeriodUnit": null,
        "rentalPricePerUnit": null
      }
    ],
    "notes": [
      { "id": "c1…", "text": "설치 희망일 9/20 오전", "createdAt": "2026-09-12T02:00:00.000Z", "parentId": null, "source": null },
      { "id": "c2…", "text": "[배송] CouriersPlease 송장번호 CP123456789 (2026-09-14)", "createdAt": "2026-09-14T00:30:00.000Z", "parentId": null, "source": { "type": "api", "keyName": "CP DIRECT 택배 예약" } }
    ]
  }
}
  • notes 는 최근 100 건만 담기며, 그 안에서 오래된 것부터 나열된다. 답글도 함께 들어 있고(100 건에 포함) parentId 가 원 메모의 id 다.
  • notes 에는 직원들이 앱에서 남긴 내부 메모가 그대로 들어 있다. 고객에게 보여 주거나 외부 로그·채팅에 그대로 남기지 말고, 개인정보와 같은 수준으로 다룬다.
  • source 가 { "type": "api" } 이면 API 로 남긴 메모다. 사람이 앱에서 쓴 메모는 null.
  • 계약서 파일, 메모 첨부, 반응, 작성자 id, 반복 작업 설정은 내보내지 않는다.

3.4 GET /api/v1/deals — 거래(계약) 목록

거래마다 짧은 고객 정보(customer)가 붙는다. 택배 예약에 필요한 받는 사람 정보는 여기서 한 번에 얻을 수 있다.

파라미터기본값설명
contractDateFrom없음yyyy-MM-dd. 이 날짜 포함 이후. 형식이 틀리거나 없는 날짜(2026-02-30)면 400.
contractDateTo없음yyyy-MM-dd. 이 날짜 포함 이전. From 보다 앞이면 400.
salesType전체판매 또는 렌탈. URL 인코딩해서 보낸다.
customerStatus전체고객 상태, 쉼표 구분(status 와 같은 값).
q없음고객 필드(3.2 와 같음) 또는 품목 이름(items[].productName)에서 찾는다.
limit / offset50 / 03.2 와 같음.
  • 정렬: contractDate 최신순 → customerId → 거래 id. 페이지를 넘겨도 순서가 흔들리지 않는다.
  • 날짜 조건을 주면 계약일이 비어 있거나 yyyy-MM-dd 가 아닌 거래는 빠진다.
curl -s -G https://modoo.com.au/api/v1/deals \
  -H "Authorization: Bearer $MODOO_API_KEY" \
  --data-urlencode "contractDateFrom=2026-09-14" \
  --data-urlencode "contractDateTo=2026-09-14" \
  --data-urlencode "salesType=판매" \
  --data-urlencode "customerStatus=Won"
{
  "data": [
    {
      "id": "7f1c…",
      "customerId": "Lx82hQ…",
      "contractDate": "2026-09-14",
      "salesType": "판매",
      "contractAmount": 1290,
      "items": [{ "productName": "Water Purifier A1", "quantity": 1, "unitPrice": 1290 }],
      "oneTimeFee": null,
      "paymentType": null,
      "isIndefinite": null,
      "rentalStartDate": null,
      "rentalEndDate": null,
      "rentalPeriod": null,
      "rentalPeriodUnit": null,
      "rentalPricePerUnit": null,
      "customer": {
        "id": "Lx82hQ…",
        "name": "Sumin Lee",
        "company": "",
        "contact": "0491 570 156",
        "email": "sumin@example.com",
        "address": "Unit 3, 12 Smith St, Sydney NSW 2000",
        "addressParts": { "streetNumber": "Unit 3/12", "streetName": "Smith St", "suburb": "Sydney", "state": "NSW", "postcode": "2000" },
        "status": "Won"
      }
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

3.5 POST /api/v1/customers/{customerId}/notes — 메모 남기기

본문:

{ "text": "[배송] CouriersPlease 송장번호 CP123456789 (2026-09-14)" }
  • text 는 앞뒤 공백을 자른 뒤 1–2000 자. 아니면 400. 본문이 JSON 이 아니어도 400.
  • 본문은 16 KB 까지. 넘으면 413.
  • 성공하면 201 과 만들어진 메모를 준다.
  • 앱의 고객 메모 카드에 바로 보이고, 작성자는 키를 발급한 관리자, 옆에 API 표시가 붙는다(마우스를 올리면 키 이름).
  • 다른 사람이 동시에 메모를 쓰거나 고쳐도 서로 덮어쓰지 않는다.
  • 앱에서 메모를 쓸 때와 달리 담당자·멘션 메일 알림은 보내지 않는다. 자동화가 건마다 메모를 남길 때 알림이 소음이 되지 않게 하기 위함이다.
  • 검색 색인에도 반영된다.
curl -s -X POST https://modoo.com.au/api/v1/customers/Lx82hQ…/notes \
  -H "Authorization: Bearer $MODOO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"[배송] CouriersPlease 송장번호 CP123456789 (2026-09-14)"}'
{
  "data": {
    "id": "c2…",
    "text": "[배송] CouriersPlease 송장번호 CP123456789 (2026-09-14)",
    "createdAt": "2026-09-14T00:30:00.000Z",
    "parentId": null,
    "source": { "type": "api", "keyName": "CP DIRECT 택배 예약" }
  }
}

4. addressParts — 주소 나누기는 최선 노력이다

MODOO 의 고객 주소는 자유 입력 한 칸(address)이다. API 는 호주 주소를 택배 칸에 맞게 나눠 addressParts 로 함께 주지만, 확실할 때만 나누고 조금이라도 애매하면 null 을 준다. 잘못 나눈 주소는 소포를 엉뚱한 곳으로 보내기 때문이다.

나누는 조건:

  • 끝이 서버브 + 주 약어 + 네 자리 우편번호 이다. 주 약어는 NSW VIC QLD WA SA TAS ACT NT(대소문자 무관). 사이 쉼표와 맨 끝의 Australia 는 있어도 된다.
  • 우편번호가 그 주의 범위에 든다(예: VIC 2000 은 null).
  • 번지로 시작한다: 12, 12A, 12-14, 3/12, Unit 3, 12, Shop 5/100 (Unit·Shop·Suite·Apt·Apartment·Flat). 호실은 Unit 3/12 처럼 streetNumber 에 함께 담긴다.
  • 거리와 서버브의 경계가 쉼표로 적혀 있거나(12 Smith St, Surry Hills NSW 2010), 쉼표가 없으면 거리 종류(St, Street, Rd, Ave, Dr, Ct, Pl, Cres, Hwy, Pde, Ln, Tce, Blvd, Cct, Cl, Way, Esp, Gr, Sq …)가 한 번만 나와 거기서 끊을 수 있다(100 George St Sydney NSW 2000).

null 이 되는 예: PO Box 12, Sydney NSW 2000, Level 5, 12 Smith St, Sydney NSW 2000, 12 Broadway Ultimo NSW 2007(쉼표도 거리 종류도 없음), 12 Smith St St Kilda VIC 3182(거리 종류가 두 번), 12 Smith St North Sydney NSW 2060(North 가 거리 이름인지 서버브인지 모름), 주 이름을 풀어 쓴 주소.

대소문자: state 는 대문자로, streetNumber 의 호실 종류·번지 뒤 글자는 Unit 3/12A 처럼 맞춘다. streetName 과 suburb 는 입력된 대소문자 그대로 준다. 주소를 모두 소문자로 적은 고객이면 "smith st", "sydney" 로 온다. CP DIRECT 에 넣기 전에 대소문자를 맞출지는 연동 쪽에서 정한다.

addressParts 가 null 이면 원문 address 를 사람이 확인해 입력하는 흐름으로 넘긴다. 추측해서 채우지 않는다.


부록 A. CP DIRECT 대량 등록(xlsx) 칸과 API 필드

GET /api/v1/deals 의 한 건(거래 + customer)을 한 줄로 옮긴다고 할 때:

CP DIRECT 칸API 필드비고
Reference Number거래 id 또는 customerId연동 쪽에서 정한다. CP DIRECT 칸 길이 제한을 확인할 것.
Receiver Name*customer.name비어 있으면 customer.company.
Receiver Contact Numbercustomer.contact
Receiver Email Address*customer.email필수 칸. 비어 있으면 예약하지 말고 사람에게 넘긴다.
Receiver Business Namecustomer.company대부분 비어 있다.
Receiver Street Number*customer.addressParts.streetNumberaddressParts 가 null 이면 사람 확인.
Receiver Street Address*customer.addressParts.streetName
Receiver Postal Code*customer.addressParts.postcode
Receiver Suburb*customer.addressParts.suburb
Receiver State*customer.addressParts.state
Item Informationitems예: items.map(i => i.productName + " x " + i.quantity).join(", ")

MODOO 에 없는 값 — 연동(스킬) 쪽 설정으로 둔다:

  • 보내는 사람(Sender) 칸 전부
  • Authority to leave: Yes
  • 무게·크기(weight / dimensions)
  • Dangerous goods: No

부록 B. 중복 예약을 막는 권장 흐름

  1. GET /api/v1/deals?contractDateFrom=…&contractDateTo=…&customerStatus=Won 으로 대상 거래를 고른다.

  2. 예약하기 전에 GET /api/v1/customers/{customerId} 의 notes 를 보고, 같은 거래에 대한 [배송] 메모가 이미 있으면 건너뛴다. (notes 는 최근 100 건이다. 메모가 아주 많은 고객은 오래전 예약 메모가 빠질 수 있다.)

  3. CP DIRECT 에서 예약한다.

  4. 예약이 끝나면 송장번호를 메모로 남긴다.

    POST /api/v1/customers/{customerId}/notes
    { "text": "[배송] CouriersPlease 송장번호 XXXX (2026-09-14)" }
    

    한 고객에게 거래가 여러 건이면 메모에 거래 id 나 품목을 함께 적어 두면 2 번에서 구별하기 쉽다.