Nodaro 문서
문서노드 레퍼런스모델템플릿AI 에이전트(MCP)개발자셀프 호스팅리서치비교
TypeScript SDK

조직과 워크스페이스

TypeScript로 Nodaro 조직과 워크스페이스를 관리합니다. 학교와 팀을 만들고 멤버를 초대하며, 참여 코드를 발급하고 크레딧 사용량 보고서를 읽습니다.

Nodaro Cloud에서 사용할 수 있습니다

client.organizations는 학교나 팀인 조직을 멤버, 초대, 감사 로그, 사용량 보고서와 함께 관리합니다. client.workspaces는 그 안의 워크스페이스(수업이나 팀 프로젝트 등)를 멤버, 참여 코드와 함께 관리합니다. 이 메서드들은 워크스페이스 REST API를 호출합니다. 개념은 워크스페이스를 참조하세요.

조직은 Nodaro Cloud 기능이며, 인스턴스별로 켭니다. 조직이 없는 인스턴스에서는 이 페이지의 모든 메서드가 NotFoundError를 발생시킵니다.

소속과 동작

워크스페이스에 속하는 것과 워크스페이스에서 동작하는 것은 서로 다릅니다. 이 두 리소스는 누가 어디에 속하는지를 관리합니다. 워크스페이스에서 동작하려면, 즉 목록을 그 워크스페이스에서 읽고 새로 만든 결과물이 그곳에 저장되게 하려면, client.withWorkspace(workspaceId)로 클라이언트를 만들거나 createClient에 workspaceId를 전달하세요.

const classroom = client.withWorkspace(workspaceId)
await classroom.workflows.run(workflowId) // runs in the class
await client.workflows.run(workflowId)    // runs in your personal space

client.me()는 내가 속한 조직과 워크스페이스, 그리고 lastWorkspaceId를 반환합니다.

모든 권한은 서버가 결정합니다. 호출자가 초대, 제거, 이름 변경을 할 수 있는지는 서버가 판단해 오류로 알려 줍니다. 설정에 따라 결과가 달라질 수 있으므로 내 클라이언트에서 짐작하지 마세요.

메서드

메서드하는 일
organizations.list()내가 속한 조직을 나열합니다
organizations.get(id)조직 하나를 읽습니다
organizations.create(input)학교나 팀을 만듭니다
organizations.update(id, input)이름이나 설정을 바꿉니다
organizations.delete(id)조직을 삭제합니다
organizations.transferOwnership(id, userId)다른 멤버를 소유자로 지정합니다
organizations.leave(id)조직에서 나갑니다
organizations.listMembers(), updateMember(), removeMember()멤버를 관리합니다
organizations.invite(orgId, input)이메일로 사람을 초대합니다
organizations.listInvitations(), revokeInvitation(), resendInvitation()보낸 초대를 관리합니다
organizations.previewInvitation(), acceptInvitation()초대를 표시하고 수락합니다
organizations.audit(orgId, opts?)감사 로그를 읽습니다
organizations.usage(), usageRows(), usageCsv()크레딧 사용량을 읽습니다
workspaces.list()내가 속한 워크스페이스를 나열합니다
workspaces.listForOrg(orgId, opts?)조직의 워크스페이스를 나열합니다
workspaces.get(id)워크스페이스 하나를 읽습니다
workspaces.create(orgId, input)워크스페이스를 만듭니다
workspaces.update(id, input)워크스페이스를 변경합니다
workspaces.setArchived(id, archived)워크스페이스를 보관하거나 복원합니다
workspaces.listMembers(), addMember(), updateMember(), removeMember()워크스페이스 멤버를 관리합니다
workspaces.getJoinCode(), actOnJoinCode()참여 코드를 관리합니다
workspaces.join(code)코드로 워크스페이스에 참여합니다
workspaces.usage(), usageRows(), usageCsv()워크스페이스의 크레딧 사용량을 읽습니다

client.organizations

organizations.list()

내가 속한 조직을 나열합니다(GET /v1/orgs).

list(): Promise<{ data: OrganizationView[] }>
const { data: orgs } = await client.organizations.list()

organizations.get(id)

조직 하나를 읽습니다(GET /v1/orgs/:id).

get(id: string): Promise<{ data: OrganizationView }>

속성

타입

const { data: org } = await client.organizations.get(orgId)
console.log(org.status) // pending, active, suspended or deleted

organizations.create(input)

나를 소유자로 하는 조직을 만듭니다(POST /v1/orgs). 새 조직은 보통 pending 상태로 시작하고, Nodaro가 승인하면 active가 됩니다. pending 상태인 동안에는 나만 조직을 볼 수 있고 조직 안의 어떤 것도 변경할 수 없으므로, 사용자에게 승인을 기다리는 중이라고 알려 주세요.

create(input: { name: string; kind: "school" | "team"; slug?: string; acceptTerms?: boolean; settings?: OrgSettings }): Promise<{ data: OrganizationView }>

속성

타입

const { data: school } = await client.organizations.create({
  name: "Sunrise School",
  kind: "school",
  acceptTerms: true,
})

acceptTerms: true가 없는 학교는 400 terms_required로 실패합니다.

organizations.update(id, input)

조직의 이름을 바꾸거나 설정을 변경합니다(PATCH /v1/orgs/:id).

update(id: string, input: { name?: string; settings?: OrgSettings }): Promise<{ data: OrganizationView }>

속성

타입

await client.organizations.update(orgId, {
  settings: { default_workflow_visibility: "workspace", allowed_email_domains: ["school.example"] },
})

설정은 계층으로 적용됩니다. 워크스페이스 설정이 조직 설정보다 우선하고, 조직 설정은 그 종류의 기본값보다 우선합니다. 키를 false로 설정하는 것은 실제 값이며, “설정 안 함”이 아닙니다.

키타입의미
admin_accessview 또는 edit관리자가 멤버의 워크플로로 할 수 있는 일
default_workflow_visibilityprivate 또는 workspace새 워크플로의 공개 범위
member_access_to_sharedview 또는 edit멤버가 워크스페이스에 공유된 워크플로로 할 수 있는 일
members_can_create_projects불리언멤버가 워크스페이스에서 프로젝트를 만들 수 있는지 여부
member_caps_enabled불리언멤버별 크레딧 한도를 적용할지 여부
personal_space_enabled불리언멤버가 개인 공간을 가질 수 있는지 여부
workspace_admins_can_invite불리언워크스페이스 관리자가 새로운 사람을 초대할 수 있는지 여부
collaborators_can_invite불리언편집자 권한의 공동 작업자가 다른 공동 작업자를 초대할 수 있는지 여부
allowed_email_domains문자열 목록조직 전용: 참여할 수 있는 이메일 도메인
vocabulary_overrides객체조직 전용: 해당 종류의 단어에 쓸 새 레이블. 예: { "workspace": "Cohort" }. 워크스페이스를 가리키는 단어가 문법상 남성형이면 "workspace_gender": "m"을 추가하세요. 그러면 히브리어와 브라질 포르투갈어 문장이 그 단어에 맞게 활용됩니다

조직 전용 키 2개는 업데이트할 때 전체가 교체됩니다.

organizations.delete(id)

조직을 삭제합니다(DELETE /v1/orgs/:id). 이전 멤버에게는 조직이 숨겨지지만, 아무것도 파기되지 않습니다. 먼저 모든 워크스페이스를 보관하세요. 그렇지 않으면 호출이 409 has_active_workspaces로 실패합니다.

delete(id: string): Promise<{ data: { id: string; status: string } }>

속성

타입

await client.organizations.delete(orgId)

organizations.transferOwnership(id, userId)

다른 멤버를 소유자로 지정합니다(POST /v1/orgs/:id/transfer-ownership). 나는 관리자가 됩니다.

transferOwnership(id: string, userId: string): Promise<{ data: { orgId: string; ownerUserId: string } }>

속성

타입

await client.organizations.transferOwnership(orgId, newOwnerId)

organizations.leave(id)

조직에서 나갑니다(POST /v1/orgs/:id/leave). 소유자는 나갈 수 없습니다. 먼저 소유권을 이전하세요. 그렇지 않으면 호출이 409 owner_cannot_leave로 실패합니다.

leave(id: string): Promise<{ data: { orgId: string; left: boolean } }>

속성

타입

await client.organizations.leave(orgId)

조직 멤버

조직의 멤버를 나열하고, 변경하고, 제거합니다(/v1/orgs/:id/members). 멤버는 active 또는 suspended 상태입니다. 정지된 멤버는 자리를 유지하지만 동작할 수 없습니다. 소유자는 정지하거나, 제거하거나, 강등할 수 없습니다.

listMembers(orgId: string, opts?: { cursor?: string; limit?: number }): Promise<OrgPage<OrgMemberView>>
updateMember(orgId: string, userId: string, input: { role?: "admin" | "member"; status?: "active" | "suspended" }): Promise<{ data: OrgMemberView }>
removeMember(orgId: string, userId: string): Promise<{ data: { removed: boolean } }>

속성

타입

const { data: members, nextCursor } = await client.organizations.listMembers(orgId)
await client.organizations.updateMember(orgId, userId, { role: "admin" })

organizations.invite(orgId, input)

이메일로 사람을 초대합니다(POST /v1/orgs/:id/invitations). 주소마다 행 하나를 반환합니다.

invite(orgId: string, input: {
  emails: string[]
  orgRole?: "admin" | "member"
  workspaceId?: string
  workspaceRole?: "admin" | "member"
}): Promise<{ data: InvitationDelivery[] }>

속성

타입

const { data: rows } = await client.organizations.invite(orgId, {
  emails: ["teacher@school.example"],
  workspaceId,
  workspaceRole: "admin",
})
for (const row of rows) {
  if (row.status !== "sent") showInviteLink(row.email, row.link) // not emailed: share the link yourself
}

링크를 보여 주세요. status가 sent가 아닌 행에는 대신 link가 들어 있습니다. 설치에 메일 공급자가 없거나 전송에 실패한 경우입니다. 어느 쪽이든 초대는 존재하며, 링크가 없으면 아무도 초대에 접근할 수 없습니다. 초대는 14일 후에 만료됩니다. 조직의 하루 초대 한도에 도달하면 RateLimitedError가 반환됩니다.

초대 관리

초대를 나열하고, 취소하고, 다시 보냅니다.

listInvitations(orgId: string, opts?: { status?: "open" | "accepted" | "revoked" | "expired"; workspaceId?: string; cursor?: string; limit?: number }): Promise<OrgPage<InvitationView>>
revokeInvitation(id: string): Promise<{ data: { id: string; revoked: boolean } }>
resendInvitation(id: string): Promise<{ data: InvitationDelivery & { id: string } }>

속성

타입

const { data: open } = await client.organizations.listInvitations(orgId, { status: "open" })
await client.organizations.resendInvitation(open[0].id)

초대 미리 보기와 수락

previewInvitation()은 공개입니다. 초대받은 사람이 아직 로그인하지 않은 상태에서도 동작하며, 주소를 가려서 반환합니다. acceptInvitation()에는 초대의 이메일과 일치하는 이메일로 로그인한 호출자가 필요합니다.

previewInvitation(token: string): Promise<{ data: InvitationPreview }>
acceptInvitation(token: string): Promise<{ data: { orgId: string; workspaceId: string | null } }>

속성

타입

const { data: preview } = await client.organizations.previewInvitation(token)
const { data: joined } = await signedInClient.organizations.acceptInvitation(token)

수락은 400 invitation_expired, invitation_revoked, invitation_accepted, email_mismatch로 실패할 수 있으며, 조직이 목록에 있는 이메일 도메인만 받는 경우에는 ForbiddenError로 실패합니다.

organizations.audit(orgId, opts?)

조직의 감사 로그를 최신 항목부터 읽습니다(GET /v1/orgs/:id/audit). 조직이 정지된 동안에도 계속 읽을 수 있습니다.

audit(orgId: string, opts?: { cursor?: string; limit?: number }): Promise<OrgPage<OrgAuditEntry>>

속성

타입

const { data: entries } = await client.organizations.audit(orgId, { limit: 50 })
for (const e of entries) console.log(e.createdAt, e.action, e.actor?.displayName ?? "system")

각 항목에는 id, workspaceId, action, targetType, targetId, details, createdAt, actor가 있으며, 시스템이 수행한 동작이면 actor는 null입니다. action은 열린 목록입니다. 알고 있는 동작은 표시하고, 나머지는 원래 문자열을 그대로 표시하세요. 고정된 목록만 처리하는 코드는 새 동작이 처음 나타날 때 깨집니다.

조직 사용량

소유자와 조직 관리자를 위한, 기간별 크레딧 사용량 보고서입니다(GET /v1/orgs/:id/usage). usage()는 보고서를 그룹으로 묶고, usageRows()는 그 보고서를 이루는 실행을 최신 항목부터 한 페이지씩 반환하며, usageCsv()는 보고서나 행을 CSV 텍스트로 반환합니다.

usage(orgId: string, opts?: { from?: string; to?: string; tz?: string; groupBy?: "workspace" | "member" | "model" | "day"; workspaceId?: string; userId?: string }): Promise<{ data: UsageReport }>
usageRows(orgId: string, opts?: { from?: string; to?: string; tz?: string; workspaceId?: string; userId?: string; cursor?: string; limit?: number }): Promise<OrgPage<UsageLogEntry>>
usageCsv(orgId: string, opts?: { from?: string; to?: string; tz?: string; groupBy?: string; workspaceId?: string; userId?: string }): Promise<string>

속성

타입

const { data: report } = await client.organizations.usage(orgId, {
  from: "2026-09-01",
  to: "2026-09-30",
  tz: "Europe/Rome",
  groupBy: "workspace",
})
const csv = await client.organizations.usageCsv(orgId, { from: "2026-09-01", to: "2026-09-30" })

각 행은 세 가지 크레딧 수치를 보고합니다. credits는 지금까지 실행에 든 크레딧입니다. 끝난 실행은 확정된 금액이고, 그 밖의 실행은 예약된 금액입니다. settledCredits와 inFlightCredits는 이 값을 나눈 것입니다. 그룹이 truncated인 경우에도 합계는 기간 전체를 포함합니다.

  • platformAbsorbedCredits는 종량제 실행이 워크스페이스의 남은 예산을 넘어선 초과분으로, 플랫폼이 부담합니다.
  • chargedToBudget은 settledCredits에서 platformAbsorbedCredits를 뺀 값으로, 예산에 반영된 확정 크레딧입니다.
  • appMarkupAbsorbedCredits는 예산으로 충당하지 못한 앱 추가 요금입니다. 보고서에 해당하는 실행이 없으므로 chargedToBudget에 포함되지 않습니다.

CSV 내보내기는 사용자당 분당 10회로 제한됩니다.

client.workspaces

workspaces.list()

내가 속한 워크스페이스와 내 lastWorkspaceId를 나열합니다(GET /v1/workspaces). 요약 정보를 반환하며, client.me()에 담긴 목록과 같습니다.

list(): Promise<{ data: WorkspaceSummary[]; lastWorkspaceId: string | null }>
const { data: workspaces, lastWorkspaceId } = await client.workspaces.list()

workspaces.listForOrg(orgId, opts?)

조직 하나의 워크스페이스를 나열합니다(GET /v1/orgs/:id/workspaces).

listForOrg(orgId: string, opts?: { includeArchived?: boolean }): Promise<{ data: WorkspaceView[] }>

속성

타입

const { data: classes } = await client.workspaces.listForOrg(orgId)

workspaces.get(id)

워크스페이스 하나의 전체 정보를 읽습니다(GET /v1/workspaces/:id).

get(id: string): Promise<{ data: WorkspaceView }>

속성

타입

const { data: workspace } = await client.workspaces.get(workspaceId)

workspaces.create(orgId, input)

조직 안에 워크스페이스를 만듭니다(POST /v1/orgs/:id/workspaces). 조직이 active 상태여야 합니다.

create(orgId: string, input: { name: string; slug?: string; description?: string; settings?: WorkspaceSettings }): Promise<{ data: WorkspaceView }>

속성

타입

const { data: classOne } = await client.workspaces.create(orgId, { name: "Class 1" })

workspaces.update(id, input)

워크스페이스를 변경합니다(PATCH /v1/workspaces/:id).

update(id: string, input: { name?: string; description?: string | null; settings?: WorkspaceSettings }): Promise<{ data: WorkspaceView }>

속성

타입

await client.workspaces.update(workspaceId, { settings: { members_can_create_projects: true } })

workspaces.setArchived(id, archived)

워크스페이스를 보관하거나 복원합니다(POST /v1/workspaces/:id/archive 또는 /unarchive). 보관은 되돌릴 수 있으며 아무것도 파기하지 않습니다. 워크스페이스는 새 작업을 받지 않게 되지만, 모든 내용을 계속 읽을 수 있습니다. 보관된 워크스페이스에 쓰기를 시도하면 ForbiddenError로 실패합니다.

setArchived(id: string, archived: boolean): Promise<{ data: WorkspaceView }>

속성

타입

await client.workspaces.setArchived(workspaceId, true)

워크스페이스 멤버

워크스페이스의 멤버를 나열하고, 추가하고, 변경하고, 제거합니다(/v1/workspaces/:id/members). 추가하려면 그 사람이 이미 조직에 속해 있어야 합니다. 새로운 사람을 들이려면 초대하세요. 조직의 소유자와 관리자는 목록에 없어도 모든 워크스페이스의 관리자입니다.

listMembers(id: string, opts?: { cursor?: string; limit?: number }): Promise<OrgPage<WorkspaceMemberView>>
addMember(id: string, input: { userId: string; role: "admin" | "member" }): Promise<{ data: WorkspaceMemberView }>
updateMember(id: string, userId: string, input: { role?: "admin" | "member"; status?: "active" | "suspended"; creditCap?: number | null }): Promise<{ data: WorkspaceMemberView }>
removeMember(id: string, userId: string): Promise<{ data: { removed: boolean } }>

속성

타입

await client.workspaces.addMember(workspaceId, { userId, role: "member" })
await client.workspaces.updateMember(workspaceId, userId, { creditCap: 500 })

조직의 활성 멤버가 아닌 사람을 추가하면 400 not_org_member로 실패하고, 같은 사람을 두 번 추가하면 409 already_a_member로 실패합니다.

참여 코드

참여 코드가 있으면 초대 없이 워크스페이스에 참여할 수 있습니다. 코드를 읽거나 변경할 수 있는 사람은 워크스페이스 관리자뿐입니다.

getJoinCode(id: string): Promise<{ data: JoinCodeView | null }>
actOnJoinCode(id: string, action: "rotate" | "enable" | "disable"): Promise<{ data: JoinCodeView }>

속성

타입

const { data: code } = await client.workspaces.actOnJoinCode(workspaceId, "enable")
console.log(code.code) // for example BCDFGHJK

아직 코드를 만든 적이 없으면 getJoinCode()는 null을 반환합니다.

workspaces.join(code)

참여 코드로 워크스페이스에 참여합니다(POST /v1/workspaces/join). 내 클라이언트가 어느 워크스페이스에서 동작하든 작동하므로, 오래된 선택 때문에 막히는 일이 없습니다.

join(code: string): Promise<{ data: { orgId: string; workspaceId: string } }>

속성

타입

const { data: joined } = await client.workspaces.join("BCDFGHJK")

알 수 없는 코드, 꺼진 코드, 보관된 워크스페이스의 코드는 모두 400 join_code_invalid로 실패합니다. 세 경우 모두 같은 응답이므로, 코드로 무엇이 존재하는지 알아낼 수 없습니다. 시도가 너무 많으면 RateLimitedError가 반환됩니다.

워크스페이스 사용량

조직과 같은 크레딧 보고서를 워크스페이스 하나에 대해 제공합니다(GET /v1/workspaces/:id/usage). 멤버는 자신의 실행을 보고, 관리자는 모든 사람의 실행을 보며 userId로 필터링할 수 있습니다.

usage(id: string, opts?: { from?: string; to?: string; tz?: string; groupBy?: "member" | "model" | "day"; userId?: string }): Promise<{ data: UsageReport }>
usageRows(id: string, opts?: { from?: string; to?: string; tz?: string; userId?: string; cursor?: string; limit?: number }): Promise<OrgPage<UsageLogEntry>>
usageCsv(id: string, opts?: { from?: string; to?: string; tz?: string; groupBy?: string; userId?: string }): Promise<string>

속성

타입

const { data: byModel } = await client.workspaces.usage(workspaceId, { groupBy: "model" })

오류

모든 권한은 서버가 결정하고, 오류로 응답합니다. SDK에서는 403이 ForbiddenError, 404가 NotFoundError, 429가 RateLimitedError가 됩니다. 이유는 각 오류의 message에서 확인하세요. 그 밖의 상태는 서버의 code를 담은 NodaroError로 전달됩니다.

상태코드발생 조건
400validation_error필드가 잘못되었거나, 커서 형식이 잘못되었거나, 사용량 보고서의 날짜, 시간대 또는 기간이 잘못되었습니다
400terms_requiredacceptTerms: true 없이 학교를 만들었습니다
400not_org_member워크스페이스에 추가한 사람이 그 조직의 활성 멤버가 아닙니다
400token_workspace_mismatch한 워크스페이스에 묶인 토큰을 다른 워크스페이스에서 사용했습니다
400join_code_invalid참여 코드가 없거나, 꺼져 있거나, 보관된 워크스페이스의 코드입니다
400invitation_expired, invitation_revoked, invitation_accepted, email_mismatch초대를 수락할 수 없습니다
403역할이 부족하거나, 멤버가 정지되었거나, 조직이 활성 상태가 아니거나, 워크스페이스가 보관되었거나, 이메일 도메인이 허용되지 않았습니다
404해당 조직, 워크스페이스, 멤버 또는 초대가 없거나, 내가 그 멤버가 아닙니다
409name_taken슬러그가 이미 사용 중입니다
409already_a_member그 사람이 이미 워크스페이스에 있습니다
409owner_cannot_leave소유자가 나가려고 했습니다
409has_active_workspaces조직에 아직 보관되지 않은 워크스페이스가 있습니다
429조직 생성, 참여 시도, CSV 내보내기 또는 초대가 너무 많습니다
503billing_unavailable이 인스턴스에서는 아직 사용량 보고서를 사용할 수 없습니다
503audit_unavailableCSV 내보내기를 감사 로그에 기록하지 못했습니다. 다시 시도하세요.

자주 묻는 질문

조직과 워크스페이스는 어떻게 다른가요?

조직은 학교나 팀이며, 소유자 한 명과 관리자, 멤버로 이루어집니다. 워크스페이스는 그 안에서 멤버들이 함께 작업하는 곳으로, 수업이나 프로젝트 팀 같은 단위입니다. 모든 워크스페이스는 하나의 조직에 속합니다.

워크스페이스 안에서 워크플로를 실행하려면 어떻게 하나요?

client.withWorkspace(workspaceId)로 그 워크스페이스용 클라이언트를 만들고, 그 클라이언트에서 run을 호출하세요. 원래 클라이언트는 계속 내 개인 공간에서 동작합니다.

새 조직이 pending 상태인 이유는 무엇인가요?

새 조직은 pending 상태로 시작하고, Nodaro가 승인하면 active가 됩니다. 그 전까지는 소유자만 조직을 볼 수 있으며, 조직 안의 어떤 것도 변경할 수 없습니다.

초대가 링크와 함께 반환되면 어떻게 해야 하나요?

초대를 보낸 사람에게 그 링크를 보여 주세요. status가 sent가 아닌 행은 이메일로 전송되지 않은 것입니다. 설치에 메일 공급자가 없거나 전송에 실패했기 때문입니다. 초대 자체는 존재하며, 그 링크가 초대에 접근하는 유일한 방법입니다.

최종 업데이트

목차