# 커넥터 — /v1/connectors > 이 키에서 어떤 연결 서비스가 열렸는지 보고, 안 열린 서비스를 부탁하고, 부탁 결과를 봅니다. > 원문: https://oneport.kr/docs/reference/connectors > **이 문서를 받은 AI 에게 — 작업 카드** > > - OpenAI 형식 SDK·도구: base_url `https://oneport.kr/v1` · 헤더 `Authorization: Bearer <키>` > - Anthropic 형식 SDK·Claude Code: base_url `https://oneport.kr` (끝에 /v1 을 붙이지 않아요 — SDK 가 /v1/messages 를 스스로 붙여요) · 헤더 `x-api-key: <키>` 또는 `Authorization: Bearer <키>` > - ❌ `Anthropic(base_url="https://oneport.kr/v1")` → ✅ `Anthropic(base_url="https://oneport.kr")` > - Vercel AI SDK: `createOpenAI({ baseURL: "https://oneport.kr/v1" })` 로 만든 제공자는 `oneport.chat("")` 로 부르세요 — 그냥 `oneport("")` 는 `/v1/responses` 로 가서 Claude · Gemini 가 400 이에요. `@ai-sdk/anthropic` 의 `createAnthropic` 은 baseURL 에 `/v1` 까지 적어요(Anthropic SDK 와 반대예요). > - API 키는 `sk-so-` 로 시작해요. 키는 코드에 적지 말고 환경변수 `ONEPORT_API_KEY` 에 넣어 읽으세요. > - ❌ 브라우저 코드에 `apiKey: "sk-so-…"` → ✅ 서버에서 `os.environ["ONEPORT_API_KEY"]` · `process.env.ONEPORT_API_KEY` 로 읽어 불러요 > - 대화에 `sk-so-` 키가 붙어 오면 코드에 박지 말고 환경변수로 옮기게 하고, 그 키는 산 곳 대시보드 AI API 화면의 키 목록에서 「새로 받기」(옛 키는 꺼져요)로 바꾸라고 알려 주세요. > - 모델 id 는 지어내지 말고 `GET https://oneport.kr/v1/models`(키 없이 열려요)의 `id` 를 글자 그대로 쓰세요. 목록은 값 순서가 아니에요 — 접두어로 첫 줄을 고르면 비싼 모델이 걸려요. 글 모델은 `kind` 가 `"chat"` 인 줄이고, 첫 호출은 `pricing.output_credits_per_mtok` 가 작은 모델로 해 보세요(10월 기준 Claude 는 claude-haiku-5-5). 회사는 `owned_by` 로 갈라요 — Claude 는 `"anthropic"`, 공식 GPT 는 `"openai"` 이면서 id 에 `/` 가 없는 줄이에요. > - ❌ `model="claude-sonnet"`(짐작한 이름 · 404 `model_not_found`) > - `endpoint` 칸이 그 모델의 기본 문이에요 — `kind` 가 `"responses"` 인 모델(pro 등급 · gpt-5.3-codex · `endpoint` 가 `"POST /v1/responses"`)은 `/v1/chat/completions` 로 못 불러요. Claude 줄은 `POST /v1/chat/completions` 이고 `/v1/messages` 로도 불러요 — 이 문은 Claude 만 받아요. > - `supports_effort` 가 `true` 인 GPT 는 생각한 토큰도 출력 상한(`max_tokens` · `max_completion_tokens`)에 들어가요 — 수백으로 두면 답이 비고 `finish_reason` 이 `"length"` 로 끝날 수 있으니 2,000 쯤에서 시작하고 `"length"` 면 올리세요. > - 목록 응답은 `{"object": "list", "data": [ … ]}`. 값 어림 — 한 번 값은 (입력 토큰 × `pricing.input_credits_per_mtok` + 출력 토큰 × `pricing.output_credits_per_mtok`) ÷ 100만 크레딧이에요(1 크레딧은 부가세 포함 2원어치예요). > - ❌ `x-should-retry: false` 응답을 반복문으로 다시 보내기 → ✅ 멈추고 `error.type` 을 확인하세요. 몸통은 OpenAI 형식 문이면 `{"error": {"type": "…", "message": "…", "code": "…"}}`, `/v1/messages` 면 `{"type": "error", "error": {"type": "…", "message": "…"}}` 예요 — 402 · 키 한도 · 강좌 몫 429 엔 `error.reason` 이 더 붙어요. 잠시 뒤 그대로 다시 보낼 실패엔 `Retry-After`(정수 초)가 대개 와요. > - ❌ `if "429" in str(e)` · `e.message` 로 분기 → ✅ Python openai 는 `e.type` · `e.body.get("reason")`, Node openai 는 `err.type` · `err.error.reason` — 429 는 키 한도 · 벤더 혼잡 · 강좌 몫 셋이라 상태 코드로 못 갈라요. Python Anthropic SDK 는 `except anthropic.APIStatusError as e:` 에서 `e.status_code` 와 `e.body["error"]["type"]`, Node 는 `err.status` 와 `err.error.error.type` 로 읽어요. 402 · 키 한도 429 의 갈래(`reason`)는 Python Anthropic SDK 면 `e.body["error"].get("reason")`, Node 면 `err.error.error.reason` 이에요. > - 남은 크레딧(지갑 전체)은 `GET https://oneport.kr/v1/credits` 의 `balance_credits` 예요(1 크레딧 아래를 버린 정수라 호출 앞뒤 차이로 쓴 몫을 세지 마세요 — 쓴 몫은 글 문 응답의 `usage.cost_credits` · 정산이 2초 안에 안 끝나면 그 칸 없이 와요). `GET https://oneport.kr/v1/key` 는 이 키에 건 한도(`month` · `day`)만 보여 주고 지갑 잔액은 없어요 — `month` · `day` 는 늘 객체이고 한도를 안 건 키면 `month.remaining_credits` · `day.remaining_credits` 가 null 이에요 · 그땐 지갑 잔액까지 써요. > - 하루 한도는 이미 쓴 몫이 한도에 닿았는지만 봐요 — 이번 달 · 강좌 한도는 이 요청 최대 몫까지 더해 재서 줄이면 통과할 수 있어요. 키 월 한도는 같은 API 키로 아직 끝나지 않은 호출 몫까지 더해 넘었으면 `error.reason` 이 `"held_by_others"` 로 와요. 묶음 처리 문(`/v1/batches`)은 없어요 — 한 건씩 보내요(동시에 보내도 되고 분당 횟수 제한도 없어요). > - 스트림 없이 부르면 답이 다 만들어져야 첫 응답이 와요 — 모델 회사로 보낸 뒤 120초 안에 안 오면 422 `upstream_execution_unconfirmed` 로 끝나요(모델 회사에서는 실행됐을 수 있어요). 그보다 긴 답은 `stream: true` 로 받으세요(스트림은 약 12분 50초에 끊겨요). SDK 시간 제한은 300초로 두세요(Python `OpenAI(…, timeout=300)` · Node `new OpenAI({ …, timeout: 300_000 })` — Node 는 밀리초 · SDK 동작). > - 첫 응답 120초 시계는 스트림이어도 같아요. 이 422 는 `x-should-retry: false` 로 와요 — 같은 요청을 자동으로 다시 보내지 마세요. stream: true 로 바꾸는 것도 새 생성이라 자동으로 하지 않아요. `Idempotency-Key` 를 실어 보낸 요청이면 받은 `x-gateway-status-url` 을 같은 API 키로 GET 조회하세요. `Idempotency-Key` 없이 보낸 요청엔 조회 주소가 안 오고 요청 ID 로 만든 주소로는 조회가 안 돼요 — 요청 ID 와 부른 시각을 admin@schoolorder.kr 로 알려 주세요. 요청 ID 는 머리글 `request-id`(같은 값이 `x-request-id`)와 오류 몸통 `error.request_id` 로 와요. 조회 주소는 `/v1/requests/` 꼴 상대 주소라 앞에 https://oneport.kr 를 붙여요. > - 고친 요청엔 새 Idempotency-Key 를 써요(같은 Idempotency-Key 로 본문을 바꾸면 409 `idempotency_conflict`). `Idempotency-Key` 를 실었으면 200 머리말을 받기 전의 연결 오류 · 시간 초과 뒤에 같은 Idempotency-Key · 같은 본문으로 다시 보내세요(몇 번 보내도 두 번 실행되지 않지만, 간격은 늘려 가며). 200 을 받은 뒤 끊긴 스트림은 잘린 답이라 같은 Idempotency-Key 는 409 예요 — 그때까지 쓴 토큰은 사용했고, 받은 글로 이어 받을 땐 새 Idempotency-Key 예요. > - 경계 사례는 이 장 전체 판(`https://oneport.kr/docs/reference/connectors.full.md`)과 「에러」 장(`https://oneport.kr/docs/errors.md`)에 있어요. 커넥터(연결 서비스)는 원포트 채팅 · 작업실이 Notion 같은 바깥 서비스의 글을 읽거나 쓰게 잇는 연결이에요. 이 장의 문 넷은 API 키로 커넥터를 보고, 안 열린 서비스를 부탁하고, 부탁 결과를 보는 문이에요. 모델을 부르는 키(sk-so-) 그대로 써요. > **이 문으로 하는 일 · 사람이 하는 일** > > 이 장의 커넥터는 모델을 부르는 문(/v1/chat/completions · /v1/messages · /v1/responses · /v1/tasks)에 실리지 않아요 — 커넥터는 원포트 채팅 · 작업실에서 써요. 채팅 · 작업실은 웹 화면이에요(키를 산 곳이나 oneport.kr 의 /ai 에 로그인해 들어가요) — 작업 문(/v1/tasks)과 달라요. 채팅에선 사람이 입력창 [+]에서 그 서비스(예: Notion)를 골라 물어야 AI 가 그 서비스를 읽어요 — 고르지 않은 질문엔 실리지 않고, 웹 검색 · 두 AI · 번역 같은 다른 도구와는 한 질문에 같이 못 켜요. 모델은 Claude 와 저희가 확인한 GPT · Gemini · Grok 모델만 받아요. 이 목록(GET /v1/connectors)에 없는 서비스는 채팅 · 작업실에도 없어요 — 부탁해서 저희가 열면 이 목록에 생기고, 공개 서비스(아래 「state · reason 으로 다음 행동 고르기」)가 아니면 채팅 · 작업실에선 지갑에서 켜야 열려요. 지금 손님께 연 서비스는 공개 서비스뿐이에요. 다만 공공 데이터(나이스 학교 찾기 · 학사일정) · Google Drive · Google Classroom(학교 판)은 채팅 도구라 이 목록에 서지 않아요 — 채팅 입력창 [+]에서 골라 쓰고(지갑 결정은 공개 서비스처럼 막기만 봐요), 학생 자리에선 닫히고, 작업실엔 없어요. 모델 요청에 직접 붙이는 원격 MCP 는 이 장의 커넥터와 다른 길이에요 — 쓰시는 MCP 서버를 요청에 직접 실어요(/v1/messages 는 tools 의 mcp_toolset 과 몸통 맨 위 mcp_servers, /v1/responses 는 tools 의 type "mcp"). 채팅에서 이은 계정은 실리지 않고, 도구 요금 없이 토큰만 사용해요. 싣는 모양(베타 머리글 · mcp_servers 칸)은 모델 회사 문서를 따르고(벤더 동작), 저희는 anthropic-beta 머리글을 그대로 넘겨요. 학생 키는 그 길도, 작업 문(/v1/tasks)도 403 student_key_blocked 예요. 스페이스 전송 정책을 켠 키는 이 칸 · 도구를 실으면 403 space_content_policy 예요. /v1/responses 에서 require_approval 로 멈춘 응답은 previous_response_id 로 잇지 못해요(400 · 요금 없음) — 앞 output 항목을 input 에 다시 실어 이어 가요(「Responses」 장 · 승인 항목 모양은 벤더 동작). API 키엔 서비스 계정을 잇는 자리가 없어서 linked 는 오지 않아요 — 쓸 수 있게 켜진 커넥터는 needs_link 로 보여요. 부탁을 확인하는 건 저희(원포트)예요. 지갑에서 서비스를 켜거나 거절하거나 막는 건 지갑 관리자 몫이에요. 지갑 관리자는 스페이스가 아닌 지갑이면 지갑 주인(구성원은 못 해요), 스페이스 지갑이면 스페이스 주인 · 관리자예요. 승인 · 거절 · 켜기 · 막기를 하는 API 문은 없어요. ## 문 넷 | 요청 | 하는 일 | 학생 키 | | --- | --- | --- | | GET /v1/connectors | 이 키에서 커넥터마다 state(지금 할 수 있는 일)와 reason(막힌 까닭)을 줘요. | 불러요 — blocked_by_admin · 저희가 부탁을 막은 not_verified 줄 말고는 모두 student_blocked | | POST /v1/connectors/requests | 서비스 하나를 부탁해요. Idempotency-Key 가 꼭 있어야 해요. | 403 student_cannot_request | | GET /v1/connectors/requests | 이 지갑의 부탁을 최근 것부터 50건까지 줘요. | 403 student_cannot_request | | GET /v1/connectors/requests/{id} | 부탁 한 줄을 줘요. | 403 student_cannot_request | 키는 머리글 Authorization: Bearer <키> 또는 x-api-key: <키> 로 실어요. 내 키가 학생 키인지는 GET /v1/key 의 audience 가 "student" 인지로 봐요(요금 없음) — "teacher" 나 null 이면 학생 키가 아니에요. 초·중·고 학교 지갑의 새 키는 「선생님」이 먼저 골라져 있어요 — 「학생」으로 만든 키로 선생님이 부탁하려면 그 키의 쓰임새를 「선생님」으로 바꿔요(바꾸는 곳은 아래 state 표 student_blocked 줄). 오류 몸통은 {"error": {"type", "code", "message", "reason", "connector_request_id", "request_id"}} 이고 type 과 code 는 같은 값이에요 — reason 은 403 connector_unavailable(커넥터 까닭)과 401 authentication_error(키 까닭 — 「인증」 장 「키가 안 열릴 때」 표)에 오고, 그 밖의 오류엔 칸이 없어요(null 로 오지 않아요). connector_request_id 는 그 403 의 reason 이 declined · not_verified 일 때만 와요 — 거절 · 막음이 적힌 그 서비스의 부탁 줄 id 라, GET /v1/connectors/requests/{id} 로 읽어 message(까닭)를 봐요. blocked_by_admin 엔 없어요. request_id 는 응답 머리글 request-id 와 같은 값이라 문의할 때 알려 주세요. message 는 사람에게 보일 문장이라 바뀔 수 있어요. 코드는 code 로 먼저 가르고, 그 안에서 reason 의 고정 이름을 보세요(아래 「오류」). ## 커넥터 목록 — GET /v1/connectors **GET /v1/connectors — 응답 칸** | 파라미터 | 받는 값 | 저희가 하는 일 | | --- | --- | --- | | `group` | {name, role} 또는 null | 스페이스 그룹 키(반 · 강좌 키)에만 와요. name 은 그룹 이름, role 은 그룹에 안 켜졌을 때(off_for_group) 켜 달라고 말할 쪽이에요 — group_admin(그 그룹 관리자) · wallet_admin(지갑 관리자). 그 밖의 키는 null 이고, 사람 이름은 오지 않아요. | | `connectors[].service` | 서비스 id(소문자 · 숫자 · 하이픈) | 부탁 응답의 matched_service 와 같은 값이에요. 줄은 저희가 손님에게 연 서비스와 이 지갑에서 켜 둔 서비스만 와요 — 목록에 없는 서비스도 부탁은 할 수 있어요. 순서는 저희 목록 순서예요. | | `connectors[].name` | 문자열 | 화면에 보이는 서비스 이름이에요. 사람이 말한 이름과 견줄 땐 service · name 을 소문자로 바꾸고 공백 · 문장부호를 빼고 견줘요 — 한글로 적은 이름은 이 목록에서 안 맞을 수 있어요. 부탁 문은 자주 쓰는 한글 이름도 맞추지만, 이미 목록에 있는 서비스여도 새 부탁을 만들어요. | | `connectors[].state` | linked · needs_link · requestable · pending · unavailable | 지금 이 키로 할 수 있는 일이에요(아래 표). 지갑에서 켰고 기한 · 그룹 · 저희 확인을 모두 통과한 줄은 state · reason 이 tools.read 쪽을 따라요(읽기 도구가 없는 서비스면 tools.write 쪽) — 그래서 쓰기만 막혀도 줄은 needs_link 로, 읽기만 막혀도 줄은 unavailable 로 보이니(그때도 tools.write 가 needs_link 면 link_url 이 와요), 도구마다 되는지는 tools 칸으로 가르세요. 그 밖의 줄은 줄 state 가 먼저 정해지고 두 칸이 그걸 따라요(requestable · pending 줄의 예외는 아래 tools 칸). | | `connectors[].reason` | off_for_group · student_blocked · student_write_blocked · blocked_by_admin · reapproval_needed · term_ended · not_verified · declined | state 가 unavailable 일 때, 그리고 거절된 뒤 다시 부탁할 수 있는 requestable 일 때(declined)만 와요. 그 밖엔 칸이 없어요. 문장이 아니라 고정 이름이라 코드는 이 값으로 가르세요. | | `connectors[].tools.read · tools.write` | {state, reason} 또는 null | 읽기 도구 · 쓰기 도구를 따로 보여 줘요 — 「읽기는 되고 쓰기만 막힘」(reapproval_needed · not_verified)이 이렇게 보여요. 줄 state 가 requestable(다시 부탁할 수 있는 declined 포함) · pending 이어도, 그 종류에 쓸 수 있는 도구가 하나도 안 남으면(정의를 바꿨으면 reapproval_needed, 저희가 정의를 아직 못 봤으면 not_verified) 그 칸만 unavailable 이에요 — 줄 state 는 그대로라 다음 행동은 그 줄대로이고, 채팅 · 작업실에선 정의가 바뀌었거나 저희가 아직 못 본 그 도구만 숨고 같은 종류의 다른 도구는 그대로 써요. 지갑 관리자 · 저희가 막은 줄, 학생 키 줄, 다시 부탁할 수 없는 declined 줄, 기한이 지났거나 그룹에 안 켜진 줄은 두 칸이 줄의 state · reason 을 그대로 따라서, 그 종류 도구가 있는지(null 인지)만 알려 줘요. reason 은 그쪽 state 가 unavailable 일 때와, 거절 뒤 다시 부탁할 수 있는 requestable(declined)일 때만 와요. 그 종류 도구가 없는 서비스면 null 이에요(쓰기 도구가 없으면 write 가 null). 쓰기 도구는 채팅 · 작업실에서 실행 전에 사람 확인을 받아요. | | `connectors[].link_url` | 주소 문자열 | tools.read · tools.write 가운데 하나라도 needs_link 면 와요 — 줄 state 와 상관없어요. 그래서 읽기 도구는 다시 확인 중(줄 state 가 unavailable)이고 tools.write 만 needs_link 인 줄에도 와요. 키를 산 곳(스쿨오더 · 오피스오더)의 그 서비스 연결 동의 화면 주소(…/ai/connect/)예요 — 서비스마다 늘 같은 주소예요. 연결은 키 · 지갑이 아니라 링크를 열고 로그인한 사람에게 붙어요 — 사람마다 따로 잇고, 그 사람이 자기 채팅 · 작업실에서 써요. 이 지갑으로 쓰게 하려면 이 지갑에 초대된 분에게 건네요 — 초대되지 않은 분은 자기 개인 지갑으로 채팅 · 작업실을 써요. 채팅 첫 화면에서 용도를 고르는 지갑이면 「공부하는 데 써요」를 골랐거나 아직 안 고른 분이 열 때 연결이 막혀요 — 학교 지갑도 개인 지갑도 같아요(「수업과 학교 업무에 써요」 · 「일이나 개인 용도로 써요」를 고른 분은 이어요). 대학 지갑은 학생 자리도 이어요. 이건 링크를 연 사람의 자리 얘기라, 이 키가 학생 키(audience "student")인지와는 따로예요. | | `connectors[].valid_until` | YYYY-MM-DD(한국 날짜) | 지갑에서 기한을 두고 켠 서비스에만 와요. 이 날이 지나면 이 문에서 unavailable · term_ended 예요. | | `connectors[].can_request_again` | true · false | reason 이 declined 인 줄엔 다시 부탁할 수 있는지가(지갑 관리자 거절이면 늘 true), 막힌 줄(blocked_by_admin · 부탁이 저희 확인에서 막힌 not_verified)엔 늘 false 가 와요. 그 밖엔 칸이 없어요. | | `connectors[].message` | 문자열 | reason 이 declined 이고 지갑 관리자가 거절하며 한 줄을 남겼을 때만 와요. 사람에게 보여 줄 문장이라 코드는 reason 으로 가르세요. 저희가 거절한 까닭은 여기 오지 않고 부탁 줄의 message 에 와요. | 503 api_error 는 저희가 상태를 잠깐 못 읽은 거예요 — 「막힌 것 없음」이나 「부탁 없음」으로 읽지 말고 잠시 뒤 다시 불러요. 학생 키로 불러도 200 이에요 — 지갑 관리자가 막은 줄(blocked_by_admin)과 저희가 부탁을 막은 줄(not_verified · can_request_again false) 말고는 모두 student_blocked 예요. 저희가 아직 안 연 서비스는 학생 키에도 줄이 없어요. ## state · reason 으로 다음 행동 고르기 까닭이 여럿이어도 reason 은 하나만 와요 — 지갑 관리자가 막은 것(blocked_by_admin)과 저희가 부탁을 막은 것(not_verified · can_request_again false)이 먼저고, 둘 다 해당하면 blocked_by_admin 이에요(부탁 문의 403 error.reason 도 같아요). 지갑에서 켠 서비스는 기한(term_ended) · 그룹(off_for_group)을 저희가 연 서비스인지(not_verified)보다 먼저 봐요 — 그래서 이 두 줄만으로는 저희가 연 서비스인지 알 수 없어요. 채팅 · 작업실에서 그 서비스가 안 보이면 원포트(admin@schoolorder.kr)에 물어보세요. tools.read · tools.write 의 state · reason 도 같은 이름이에요. 이 표는 이 키로 본 이 문의 상태예요. 원포트 채팅 · 작업실은 공개 서비스(Notion · Kahoot! · Padlet · Replit · Canva · Suno)면 지갑 결정 가운데 막기(blocked_by_admin)만 봐요 — 기한(term_ended) · 그룹(off_for_group) · 부탁 결과(pending · declined · 부탁이 막힌 not_verified) · 지갑 관리자 거절은 보지 않아서, 이런 줄이어도 채팅 · 작업실에선 그대로 써요. 그 밖의 서비스는 저희가 손님께 열었고 이 지갑에서 켜졌고(enabled) 기한 안이며 그 사람의 그룹에 닿을 때만 채팅 · 작업실에서 열려요. 어느 서비스든 쓰는 사람이 학생 자리면 닫히고, 저희가 도구를 다시 확인하는 중이면(tools 칸 reapproval_needed · not_verified) 그 종류 도구는 숨어요. 저희가 아직 손님께 열지 않은 서비스도 있어요(Zoom) — 채팅 · 작업실에 없고, 이 목록엔 지갑에서 켰을 때만 unavailable 줄로 보여요. 채팅은 닫힌 서비스를 입력창 [+] 목록에서 빼기만 하고 까닭은 적지 않아요. 작업실은 AI 가 지갑에서 닫힌 서비스를 부르면 막혔는지 · 아직 안 켜졌는지만 답에 적어요. 자세한 까닭은 이 문의 reason 으로 봐요. | state | reason | 다음 행동 | | --- | --- | --- | | linked | — | 할 일 없어요. | | needs_link | — | link_url 을 사람에게 건네세요 — 원포트 채팅 · 작업실에서 그 서비스를 쓸 사람이에요. 주소엔 비밀값이 없어 메신저 · 메일로 보내도 되고, 기한도 없고 한 번만 쓰는 주소도 아니에요. 연 사람이 자기 계정으로 로그인해 저희 동의 화면에서 「<서비스 이름>에서 계속」(예: 「Notion에서 계속」)을 누르면 그 서비스의 로그인 · 허용 화면으로 넘어가고, 허용하면 내 계정 「연결한 서비스」로 돌아와요. 이 키로 잇는 API 문은 없고, 이어도 이 키의 줄은 needs_link 그대로예요. | | requestable | — | 공개 서비스면 부탁할 필요 없어요 — 사람에게 원포트 채팅 · 작업실에서 쓰면 된다고 알려 주세요. 그 밖의 서비스면 지갑 관리자에게 물어보세요. 처음 쓸 땐 사람마다 그 서비스 계정을 이어요 — 이 줄엔 link_url 이 없지만 키를 산 곳의 같은 동의 화면에서도 이어요(스쿨오더 https://schoolorder.kr/ai/connect/ · 오피스오더 https://officeorder.co.kr/ai/connect/) — 이어도 이 키의 줄은 requestable 그대로예요(API 키엔 연결이 안 보여요). 부탁(POST /v1/connectors/requests)은 이 목록에 없는 서비스에 써요. | | requestable | declined | 저희 거절이면 부탁 줄의 message 를 보고 고쳐서 다시 부탁해요. 지갑 관리자 거절이면 이 줄의 message 를 사람에게 보여 주세요 — 다시 부탁하면 저희 확인 없이 approved 새 부탁 줄이 서고, 지갑 관리자가 다시 정할 때까지 이 줄은 pending 이에요. 그다음 지갑 관리자가 고를 수 있는 건 켜기 · 다시 거절 · 막기예요 — 모두 켜면 needs_link(몇몇 그룹에만 켜면 그 밖의 키는 off_for_group), 다시 거절하면 이 줄이 requestable · declined 로 돌아오고(그때 남긴 한 줄은 message 로), 막으면 blocked_by_admin 이 돼 채팅 · 작업실에서도 빠져요. 지갑 관리자에게 물어보세요. | | pending | — | 공개 서비스면 사람에게 원포트 채팅 · 작업실에서 쓰면 된다고 알려 주세요. 부탁 결과는 GET /v1/connectors/requests 로 그 부탁을 찾아 봐요. 같은 서비스를 또 부탁하면 그 줄로 합쳐져요(200 — 지갑 관리자를 기다리는 approved 줄은 같은 그룹의 키끼리 · 그룹 없는 키끼리만). 그 부탁이 approved 면 이 문의 줄은 지갑 관리자가 다시 정할 때까지 pending 이에요 — 바꾸려면 지갑 관리자에게 물어보세요. | | unavailable | blocked_by_admin | 지갑 관리자에게 물어보세요 — 지갑 관리자가 그 서비스를 다시 켜면 풀려요. 부탁해도 403 이고 API 로는 못 풀어요. | | unavailable | not_verified | 원포트(admin@schoolorder.kr)에 물어보세요. 지갑 관리자가 풀 수 있는 게 아니에요. | | unavailable | off_for_group | group.role 쪽에 이 그룹에도 켜 달라고 해요 — group_admin 이면 그 그룹 관리자, wallet_admin 이거나 group 이 null 이면 지갑 관리자예요. | | unavailable | student_blocked | 선생님(키를 나눠 준 분)께 말해요. 학생 키는 부탁 · 부탁 목록 문도 403 student_cannot_request 예요. 키 쓰임새를 「선생님」으로 바꾸면 다음 요청부터 풀려요 — 학교 지갑 키는 지갑 주인이 대시보드 AI API 화면 키 목록의 쓰임새 칩에서(관리 키면 PATCH /v1/keys/{id} 에 "audience": "teacher"), 스페이스 키는 스페이스 주인 · 관리자가 그 키 줄의 칩에서 바꿔요. 바꿔도 API 키로 커넥터를 쓰진 못해요 — 열린 서비스는 원포트 채팅 · 작업실에서 써요. | | unavailable | student_write_blocked | 쓰기 없이 읽기만 써요. | | unavailable | term_ended | 기한을 늘리려면 지갑 관리자에게 물어보세요. | | unavailable | reapproval_needed | 부탁할 필요 없어요. 나중에 다시 불러 보세요. 읽기 도구만 다시 확인 중이고 쓰기 도구는 이을 수 있으면 이 줄에도 link_url 이 와요 — 쓰기 도구를 쓸 사람에게 건네요. | | unavailable | declined | 부탁 줄의 message 를 보세요. 다시 부탁하면 403 connector_unavailable(reason declined)이에요. 다시 부탁할 수 없게 거절한 건 저희라 원포트(admin@schoolorder.kr)에 물어보세요 — 지갑 관리자 거절은 늘 다시 부탁할 수 있어서 이 줄이 되지 않아요. | ## 부탁하기 — POST /v1/connectors/requests **POST /v1/connectors/requests — JSON 본문과 요청 헤더** | 파라미터 | 받는 값 | 저희가 하는 일 | | --- | --- | --- | | `Authorization 또는 x-api-key` (필수) | Bearer sk-so-… 또는 sk-so-… | 부탁은 그 키가 속한 지갑에 쌓여요. 학생 키는 403 student_cannot_request 예요. | | `Idempotency-Key` (필수) | 공백 · 제어문자 없는 ASCII(영문 · 숫자 · 기호) 8~200자 | 커넥터 부탁 문(POST /v1/connectors/requests)은 Idempotency-Key 가 꼭 있어야 해요(없거나 꼴이 틀리면 400) — 글 문의 409 request_already_exists 와 조회 주소(/v1/requests/)는 이 문엔 없어요. 새 부탁이면 201, 이미 있던 줄이면 200 으로 부탁 한 줄이 오고, 결과는 그 줄의 state 칸(received · reviewing · approved · declined · blocked)으로 봐요. 같은 API 키로 같은 값 · 같은 본문을 다시 보내면 새 줄 없이 그 부탁을 200 으로 돌려줘요 — 그사이 결과가 나왔어도(approved · declined · blocked) 그 줄이 와요(Location 은 /v1/connectors/requests/{id}). 그 줄이 declined 이고 can_request_again 이 true 면 고쳐서 새 값으로 다시 부탁해요 — 같은 값이면 그 거절 줄이 또 와요. 받은 값은 기한 없이 기억해요 — 지난 부탁에 쓴 값을 새 부탁에 다시 쓰면 새 줄이 아니라 그 옛 줄(200)이나 409 가 와요. 같은 값으로 본문을 바꾸면 409 idempotency_conflict 예요 — 그 값으로 먼저 보낸 부탁은 이미 접수돼 있어요. x-should-retry: false 가 붙어서 SDK 가 스스로 다시 보내지 않아요(이 문엔 SDK 함수가 없어서, SDK 의 저수준 호출로 부를 때 얘기예요). 400 · 401 · 403 · 413 · 429 거절은 그 값에 남지 않아서 고친 요청에 같은 값을 써도 돼요. 503 api_error 를 받았거나 응답을 못 받았으면 같은 값 · 같은 본문으로 다시 보내요 — 저희가 이미 받은 부탁이면 새 줄 없이 그 줄이 200 으로 와요. 칸 순서 · 앞뒤 공백만 다른 건 같은 본문이에요. 다른 부탁엔 다른 값을 써요. 받은 값은 API 키마다 따로 기억해요 — 「새로 받기」로 바꾼 키로 같은 값을 보내면 새 부탁으로 읽고 전체 판 합치기 규칙대로 해요. 키를 바꿨으면 먼저 GET /v1/connectors/requests 로 앞 부탁을 찾아요. | | `service_name` (필수) | 1~100자 문자열 | 서비스 이름을 사람 말로 적어요. 대소문자 · 띄어쓰기 · 문장부호는 빼고 견주고, 덧말은 낱말 단위로 앱 · 어플 · app · 연동 · 연결 · 커넥터 · connector · mcp · 서비스 · service 만 빼요 — 「Padlet 게시판」처럼 다른 낱말이 붙으면 Padlet 과 다른 부탁으로 쌓여요. 자주 쓰는 한글 이름 몇몇(노션 · 카훗 · 패들렛 · 레플릿 · 캔바 · 구글 클래스룸 · 구글 드라이브 등)은 같은 서비스로 맞춰요 — 회사가 쓰는 이름(영어)으로 적으면 가장 잘 맞아요. 그 밖의 한글 이름(예: 「시소」)은 영어 이름과 다른 부탁으로 따로 쌓여요. 저희가 손님에게 연 서비스와 맞으면 응답 matched_service 에 그 id 가 와요. | | `reason` | 500자까지 문자열 또는 null | 왜 필요한지 한두 문장이에요. 저희가 확인할 때 보고, 지갑 관리자가 정할 때도 부탁한 쪽 역할과 같이 보여요. 만든 응답과 한 줄 조회에만 실리고 목록엔 안 실려요. GET /v1/connectors 줄의 reason · 403 의 error.reason(막힌 까닭)과는 이름만 같은 다른 칸이에요. | | `그 밖의 칸` | 받지 않아요 | 모르는 칸이 있으면 400 이에요. JSON 본문 전체는 8,000바이트까지예요(넘으면 413 invalid_request_error). | **부탁하기 — 응답을 못 받았으면 같은 Idempotency-Key 로 다시** ```bash curl https://oneport.kr/v1/connectors/requests \ -H "Authorization: Bearer $ONEPORT_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: jotform-request-001' \ -d '{"service_name":"Jotform","reason":"설문 응답을 모아 보려고요"}' ``` ## 부탁 결과 보기 — GET /v1/connectors/requests · /{id} **부탁 한 줄(object connector_request)의 칸** | 파라미터 | 받는 값 | 저희가 하는 일 | | --- | --- | --- | | `id` | UUID | Location 의 끝과 같은 값이에요. GET /v1/connectors/requests/{id} 로 그 줄을 다시 읽어요. | | `service_name` | 문자열 | 보낸 이름 그대로예요(앞뒤 공백만 빼요). 같은 줄에 합쳐졌으면 처음 부탁한 쪽이 적은 이름이에요. | | `matched_service` | 서비스 id 또는 null | 저희가 손님에게 연 서비스와 맞을 때만 와요 — GET /v1/connectors 의 service 와 같은 값이에요. 저희가 확인하면서 맞춰 넣으면 나중에 채워질 수 있어요. | | `state` | received · reviewing · approved · declined · blocked | 부탁의 결과예요(아래 표). GET /v1/connectors 의 state 와 이름이 다른 별개의 값이에요. | | `message` | 문자열 | state 가 declined 면 와요(저희는 거절할 때 까닭을 꼭 적어요). blocked 는 저희가 까닭을 적었을 때, approved 는 이 부탁 뒤에 지갑 관리자가 거절하며 한 줄을 남겼을 때만 와요. 사람에게 보여 줄 문장이에요. | | `can_request_again` | true · false | state 가 declined · blocked 일 때만 와요. blocked 면 늘 false 예요. | | `requested_by.role` | owner · admin · group_admin · member · wallet_key · group_key | 부탁한 쪽의 역할만 와요(사람 이름 · 키 이름은 안 와요). API 키로 낸 부탁은 wallet_key(지갑 키) · group_key(스페이스 그룹 키)예요. 모르는 값은 건너뛰게 짜세요. | | `reason` | 문자열 또는 null | 부탁한 까닭이에요. 만든 키의 응답에만 와요 — 목록 줄엔 이 칸만 없고, message · can_request_again · wallet_decision 은 목록에도 와요. | | `created_at · updated_at` | ISO 8601 시각 | 부탁을 처음 받은 때와 마지막으로 바뀐 때예요. 저희가 확인하며 그 줄을 바꾸면(state · matched_service) updated_at 이 바뀌어요 — 지갑 관리자 결정으론 안 바뀌니, 그건 wallet_decision 과 GET /v1/connectors 줄로 봐요. | | 부탁 state | 다음 행동 | | --- | --- | | received | 기다려요. | | reviewing | 기다려요. | | approved | 그 message 가 있으면 사람에게 보여 주세요. matched_service 가 오면 GET /v1/connectors 의 그 service 줄대로 해요(공개 서비스의 requestable 이면 원포트 채팅 · 작업실에서 써요). matched_service 가 null 이면 저희가 공개하기 전이에요 — 기다리거나 원포트(admin@schoolorder.kr)에 물어보세요. 다시 부탁해도 저희 확인을 다시 거치지 않고 지갑 관리자 차례로 가요(위 「부탁하기」). | | declined | can_request_again 이 true 면 고쳐서 다시 부탁할 수 있어요. false 면 다시 부탁해도 403 connector_unavailable(reason declined)이에요. | | blocked | 그 서비스가 GET /v1/connectors 목록에 있으면 not_verified 로 보여요 — 저희가 아직 안 연 서비스면 줄이 없어요. 같은 서비스를 다시 부탁하면 403 connector_unavailable(reason not_verified)이에요. 원포트(admin@schoolorder.kr)에 물어보세요. | 목록(GET /v1/connectors/requests)은 {"object": "list", "data": [ … ], "has_more": true · false} 이고, 이 지갑의 부탁을 최근 것부터 50건까지 줘요(다른 키가 낸 부탁도 들어요). has_more 가 true 면 더 오래된 부탁이 있다는 뜻이에요 — 다음 쪽을 넘기는 칸은 없으니 보관해 둔 id 로 한 줄씩 읽어요. 건수 칸은 없어요. 목록의 service_name 은 보낸 글자 그대로라, 이름으로 견주면 「구글클래스룸 앱」처럼 적힌 부탁을 놓칠 수 있어요 — 부탁하기 전에 목록을 이름으로 뒤질 필요는 없어요(결과를 기다리는 부탁 · 확인이 끝난 부탁엔 전체 판 합치기 규칙대로 이어져요). 결과는 저희가 확인한 뒤 바뀌고, 바뀌어도 메일 · 웹훅으로 알려 드리지 않아요 — 짧은 간격으로 되풀이해 부르지 말고, 사람에게 부탁했다고 알린 뒤 나중에 다시 보세요. id 가 UUID 꼴이 아니거나 이 지갑의 부탁이 아니면 404 not_found_error 예요. ## 오류 | 상태 | code | 할 일 | | --- | --- | --- | | 401 | authentication_error | 키 · 머리글을 확인해요. | | 400 | invalid_request_error | 고쳐서 보내요. | | 400 | reason_required | reason 에 어디에 쓰는지 적어 다시 보내요. | | 413 | invalid_request_error | reason 을 줄여요. | | 403 | student_cannot_request | 선생님께 말해요. | | 403 | connector_unavailable | 위 표의 그 reason 줄대로 해요. 다시 보내도 같아요. connector_request_id 가 오면 그 id 로 GET /v1/connectors/requests/{id} 를 읽어 message 를 봐요. | | 404 | not_found_error | 받은 id · Location 을 확인해요. | | 409 | idempotency_conflict | 처음 본문 그대로 보내거나, 다른 부탁이면 다른 값을 써요. | | 429 | too_many_open | 앞 부탁의 결과가 나온 뒤 다시 보내요. | | 503 | api_error | GET 은 잠시 뒤 다시 불러요 · POST 는 같은 Idempotency-Key 로 다시 보내요. 이 장의 문엔 Retry-After 가 오지 않아요 — 몇 초 쉬고 다시 해요. | ## 값 이 장의 문 넷은 요금이 안 붙어요. 채팅 · 작업실에서 커넥터가 가져온 글은 모델 입력 토큰으로 세요 — 커넥터 자체 요금은 없어요. 채팅 한 질문에서 커넥터는 4번까지 읽고, 한 번 읽은 글은 8,000자까지 모델에 실어요 — 넘으면 뒤가 잘려요(Google Drive 의 긴 글 · 파일은 첨부로 실려요). 작업실은 한 번 읽은 글을 20,000자까지 실어요. 긴 페이지를 통째로 요약하려면 그 서비스의 API 로 글을 직접 읽어 글 문(/v1/chat/completions 등)에 나눠 보내요. 더 자세한 경계 사례는 전체 판 https://oneport.kr/docs/reference/connectors.full.md 의 「커넥터 목록 — GET /v1/connectors」 · 「state · reason 으로 다음 행동 고르기」 · 「부탁하기 — POST /v1/connectors/requests」 · 「부탁 결과 보기 — GET /v1/connectors/requests · /{id}」 · 「오류」 · 「예제 — 목록을 보고, 없으면 부탁하고, reason 으로 가르기」 · 「서비스를 만드는 회사라면」 에 있어요.