---
title: "개발자 가이드"
description: "Quire REST API, OAuth, 웹훅, MCP 서버, 확장 기능."
image: "https://docs.quirelms.com/og.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.quirelms.com/ko/llms.txt
> Use this file to discover all available pages before exploring further.

# 개발자 가이드

<span id="developer-guide"></span>

조직의 API 주소와 스코프가 지정된 자격 증명을 사용하세요. 읽기 요청부터
시작해 응답을 확인하고, 시크릿은 소스 관리와 문서 예제 밖에 두세요.

Quire에는 공개 API가 하나 있습니다. HTTPS 위의 REST이며 OpenAPI 3.1 문서로
설명되고, 이벤트에는 서명된 웹훅, AI 어시스턴트에는 MCP 서버가 있습니다.
[API 레퍼런스](https://docs.quirelms.com/api/)에 모든 엔드포인트와 이벤트가 나열됩니다.

## 주소 <!--quire:addresses-->

각 조직은 고유한 주소를 가지며 API는 그 아래에 있습니다.

```
https://acme.quirelms.com/api/v1/courses
```

어느 조직의 주소에서 호출하느냐에 따라 자격 증명이 조직을 결정합니다. 한
조직의 키를 다른 조직의 주소에서 쓰면 거부됩니다.

OpenAPI 문서는 모든 조직 주소의 `/api/v1/openapi.json`에서 제공되므로
클라이언트 생성기는 항상 호출하는 버전을 보게 됩니다.

## 인증 <!--quire:authentication-->

**API 키**는 스크립트와 서버 간 연동용입니다. 관리자가
`/admin/integrations/api-keys`에서 하나를 만들고, 스코프를 고르며, 한 번만
확인할 수 있습니다. bearer 토큰으로 보내세요.

```
curl -H "Authorization: Bearer qk_live_..." https://acme.quirelms.com/api/v1/users?limit=50
```

키는 `qk_live_` 또는 `qk_test_`로 시작합니다. 연동마다 자체 키를 주세요.

**OAuth 2.1**은 로그인한 사람처럼 행동하는 애플리케이션용입니다.
`/admin/integrations/oauth-clients`에 클라이언트를 등록한 뒤, PKCE가 있는
인증 코드 플로우(`/oauth/authorize`, `/oauth/token`)를 쓰거나, 기계용
클라이언트에는 클라이언트 자격 증명을 쓰세요. 디스커버리는
`/.well-known/oauth-authorization-server`에 있습니다. 스코프는 토큰이 할 수
있는 것을 좁힐 뿐, 사람보다 더 할 수 있게 만들지 않습니다.

스코프는 `resource:read`, `resource:write`, `resource:delete` 형태이며, 예를
 들어 `courses:read`나 `enrolments:write`입니다. 네 개는 특권이며 동의 화면에
경고와 함께 표시됩니다. `audit:read`, `roles:write`, `tenants:write`,
`users:delete`가 그렇습니다.

<figure class="quire-shot" lang="en" dir="ltr"><img src="/screenshots/admin-api-keys.webp" alt="The API keys page with one key, the person it acts as, its scopes and its status, and a form to create another." width="944" height="700" loading="lazy" decoding="async"><figcaption>API keys list who each key acts as and what it may reach.</figcaption></figure>

## 요청 <!--quire:requests-->

- **페이지네이션**: 모든 목록은 커서 기반입니다. `limit`을 넘기고,
  `next_cursor`를 `page`에서 가져와 `cursor`로 넘기면서 `has_more`가
  true인 동안 반복하세요(아래 예시). 오프셋은 없습니다.
- **변경 시점**: `updated_since`는 특정 시점 이후 바뀐 것을 반환합니다.
  `include_deleted=true`와 짝을 짓거나 `/<resource>/deletions`를 읽어 무엇이
  삭제되었는지 확인하세요.
- **외부 식별자**: 대부분의 리소스는 자체 `external_id`를 받고,
  `/<resource>/ext:{external_id}`는 그것으로 읽거나 upsert합니다. 동기화가
  Quire의 식별자를 저장할 필요가 없습니다.
- **멱등성**: `Idempotency-Key` 헤더를 `POST`, `PATCH`, `DELETE`에 보내세요.
  같은 키로 재시도하면 작업을 두 번 하는 대신 첫 응답을 돌려받습니다.
  벌크 엔드포인트는 이를 요구합니다.
- **버전**: 메이저 버전은 경로에 있습니다(`/v1`). 그 안에서 각 파괴적 변경은
  날짜가 붙은 개정이며, `Quire-Version` 헤더로 고릅니다. 예:
  `Quire-Version: 2026-09-20`. 헤더가 없으면 자격 증명이 발급된 시점의
  개정을 받습니다.

목록의 한 페이지:

```
{"data": [...], "page": {"next_cursor": "eyJ2Ijox...", "has_more": true, "limit": 100}}
```

## 오류 <!--quire:errors-->

모든 오류는 RFC 9457 문제 문서입니다.

```
{"type": "https://quire.com/errors/enrolment.seat_limit_reached",
 "title": "Seat limit reached", "status": 409,
 "code": "enrolment.seat_limit_reached", "category": "conflict",
 "detail": "The course has no seats left, so this enrolment was not created. ...",
 "request_id": "01JB7XQK4Z..."}
```

분기는 안정적인 `code`로 하세요. `detail`은 사람을 위해 쓰여 그들에게
보여줘도 안전하지만 바뀔 수 있습니다. 코드를 모르겠다면 `category`로
묶으세요.

| 카테고리 | 상태 | 재시도 |
| --- | --- | --- |
| `validation` | 422, `errors`에 필드 상세 | 아니요 |
| `authentication` | 401 | 아니요 |
| `authorization` | 403 | 아니요 |
| `not_found` | 404 | 아니요 |
| `conflict` | 409 | 가끔 |
| `precondition` | 412 | 아니요 |
| `quota` | 플랜은 402, 크기는 413 | 아니요 |
| `rate_limit` | 429, `Retry-After` 동반 | 예 |
| `upstream` | 502 또는 504 | 예 |
| `internal` | 500 | 예 |

지원팀에 문의할 때는 `request_id`를 인용하세요.

## 웹훅 <!--quire:webhooks-->

`/admin/webhooks`에서, 또는 API로 `/webhook_subscriptions`에서 구독하세요.
이벤트는 이름(`enrolment.created`), 영역(`enrolment.*`), 또는 전체(`*`)로
고를 수 있습니다. Quire는 먼저 `webhook.ping`을 보내고, 엔드포인트가
응답하면 구독이 시작됩니다.

전달은 Standard Webhooks 사양을 따릅니다.

```
POST /hooks/quire
webhook-id: 01JB7XQK4Z8FQ2M3N4P5R6S7T8
webhook-timestamp: 1790000000
webhook-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
```

전달을 검증하려면:

1. JSON 파싱 전에, 받은 그대로의 바이트로 문자열
   `{webhook-id}.{webhook-timestamp}.{raw body}`를 만드세요.
2. 구독 비밀로 HMAC-SHA256을 계산하고 base64로 인코딩하세요.
3. 각 `v1,` 값을 `webhook-signature`와 상수 시간에 비교하세요. 비밀
   로테이션 중에는 두 개가 있을 수 있으며, 어느 쪽이든 일치하면 유효합니다.
4. 시계와 5분 이상 차이나는 타임스탬프는 거부하세요.

```
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(secret, id, timestamp, rawBody, header) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = createHmac('sha256', Buffer.from(secret.replace(/^whsec_/, ''), 'base64'))
    .update(`${id}.${timestamp}.${rawBody}`).digest();
  return header.split(' ').some((part) => {
    const [version, value] = part.split(',');
    const given = Buffer.from(value ?? '', 'base64');
    return version === 'v1' && given.length === expected.length && timingSafeEqual(given, expected);
  });
}
```

`webhook-id`로 중복을 제거하세요. 전달은 여러 번 올 수 있습니다. 본문에는
식별자와 짧은 요약이 담겨 있으니 현재 상태는 리소스를 가져오세요. 실패한
전달은 최대 72시간 동안 백오프를 두고 재시도되며, 전달 로그에서 재생할 수
있습니다.

## MCP <!--quire:mcp-->

Quire의 MCP 서버는 조직 주소의 `/mcp`에 있으며 스트리머블 HTTP를 사용합니다.
MCP 클라이언트는 `/.well-known/oauth-protected-resource`에서 OAuth 서버를
발견하고, 사람은 다른 OAuth 클라이언트와 마찬가지로 로그인하고 동의합니다.
도구는 그 사람의 권한으로 행동하며, 파괴적인 도구는 확인을 요청합니다.
관리자는 `/admin/integrations/mcp`에서 어떤 도구를 쓸지 고릅니다.

<figure class="quire-shot" lang="en" dir="ltr"><img src="/screenshots/admin-mcp.webp" alt="The AI assistants page with the server address to give an assistant and a table of the tools it can use." width="944" height="700" loading="lazy" decoding="async"><figcaption>AI assistants (MCP): the server address, and the tools an assistant may call.</figcaption></figure>

## 플랜과 API <!--quire:plans-and-the-api-->

API 키, OAuth 클라이언트, 웹훅, MCP 서버는 플랜의 API 권한에 속하며 모든
표준 플랜에 포함됩니다. 없는 플랜에서는 키·클라이언트·구독 만들기가
거부되고, REST 쓰기와 MCP 연결이 거부되며, 데이터를 내보낼 수 있도록 REST
읽기는 계속 동작합니다. 거부 응답은 `commerce.plan_entitlement` 코드를 가진 `precondition`
카테고리의 문제 문서입니다.

## 확장 기능 <!--quire:extensions-->

Quire 자체의 활동 유형, 블록, 등록 방식, 로그인 방식, 문항 유형, 리포트,
테마, 연동은 셀프호스트 설치가 추가할 수 있는 같은 확장 레지스트리를 통해
선언됩니다. 확장 기능은 컴파일될 때 포함됩니다. 런타임 플러그인 로더가
없고, 호스팅 조직은 하나를 추가할 수 없습니다. 관리자는
`/admin/extensions`에서 조직별로 각 확장 기능을 켜거나 끕니다
([관리자 가이드](/ko/admin/extensions/) 참조).

하나를 쓰려면 `packages/integration/extensions/src/sample.ts`의 샘플 블록과
테마에서 시작하세요. 확장 지점을 고르고 `points.ts`에서 그 계약을 읽은 뒤,
id, 버전, 라이선스, 제공하는 것과 요구하는 것, 조직이 끌 수 있는지를 담아
확장 기능을 선언하세요. 웹 애플리케이션과 워커가 조립되는 곳에 등록해
둘 다 동일하게 보장하세요. 레지스트리는 빌드될 때와 `register`를 호출할 때
각 지점의 규칙을 검사하고, 문제가 모두 명시된 잘못된 집합은 거부하며, 그럴
때는 레지스트리를 바꾸지 않습니다. 확장 기능 자체의 테스트는
`extensionContractProblems`가 비어 있는지, 그리고 끄면 영향받는 것이
바뀌는지 단정해야 합니다.

Source: https://docs.quirelms.com/ko/developer/index.mdx
