Skip to content
언어: 한국어

개발자를 위한 API

요청 한도: 모든 한도와 헤더

2026년 11월 1일 업데이트 4분 분량

LinkHarbor API가 분당 허용하는 요청 수, 요청의 최대 크기, 헤더를 읽는 방법, 429를 올바르게 처리하는 방법을 설명합니다.

모두에게 빠른 API를 유지하기 위해 LinkHarbor는 분당 요청 수를 제한합니다. 한도는 플랜과 관계없으며 월간 API 호출 할당량은 없습니다. 이 글에서는 모든 한도를 정리하고 코드에서 깔끔하게 처리하는 방법을 보여 줍니다.

요청을 세는 방법

  • 모든 요청은 정확히 하나의 버킷에 들어감. 예를 들어 인증된 요청은 user, 토큰 교환은 token
  • 1분 단위(UTC)로 세며, 새로운 1분이 시작될 때마다 카운터가 0에서 다시 시작됨
  • 버킷에 따라 워크스페이스, 사람, 연결, IP 주소 단위로 셈
  • 토큰, 스코프, 스키마에서 이미 실패하는 요청(401, 403 insufficient_scope, 400, 스키마 검사의 422 validation_failed)은 세지 않음. 403 feature_not_available 같은 이후 단계의 오류는 셈

모든 한도

버킷(X-RateLimit-Bucket)한도집계 단위적용 대상
user분당 600회API 키의 토큰은 워크스페이스(워크스페이스의 모든 키와 AI 도구 합산), 대시보드 로그인은 사람자체 버킷이 없는 /v1의 모든 인증된 요청
token분당 30회IP 주소POST /v1/token
public분당 120회IP 주소GET /v1/plans, GET /v1/pages/available 같은 인증 없는 API 요청
callback분당 120회IP 주소POST /v1/callbacks/sale
internal분당 120회IP 주소“Custom (API)” 연동의 Ingest URL(POST /v1/ingest/{id})
mcp분당 120회AI 연결(액세스 토큰)/mcp의 MCP 서버
oauth분당 300회IP 주소AI 어시스턴트용 OAuth 로그인과 디스커버리
auth분당 30회IP 주소로그인, 가입, 비밀번호 재설정(대시보드와 앱)
refresh분당 120회IP 주소대시보드 세션 연장
discover분당 60회사람, 로그인하지 않았으면 IP 주소앱의 둘러보기
subs_read분당 120회사람, 로그인하지 않았으면 IP 주소팔로우하는 페이지 읽기(앱)
subs_write분당 20회사람, 로그인하지 않았으면 IP 주소팔로우하는 페이지 변경(앱)

워크스페이스당 하나의 예산

같은 워크스페이스의 API 키 토큰은 키를 몇 개 만들든 모두 분당 600회를 함께 씁니다. 연결된 AI 어시스턴트의 도구도 내부적으로 API를 호출하므로 여기에 포함됩니다. 따라서 키를 늘려도 한도는 올라가지 않습니다. 부하가 여러 프로그램에 분산된다면 공유 예산을 기준으로 계획하세요.

헤더

X-RateLimit-Limit
버킷의 분당 한도입니다. 예: 600.
X-RateLimit-Remaining
현재 1분 동안 남은 요청 수입니다.
X-RateLimit-Bucket
요청이 들어간 버킷입니다. 예: user.
Retry-After
429 rate_limited에서만 있습니다: 다음 1분이 시작될 때까지의 초입니다. api_key_locked에서는 대기 시간이 details.retry_after_sec에 있습니다.

한도에 도달하면

한도에 도달하면 API가 HTTP 429와 다음 본문으로 응답합니다. message의 텍스트는 계정 언어로 표시되며(API 오류 형식 참고), 여기서는 영어로 보여 줍니다. 코드 rate_limited를 기준으로 처리하세요.

429 응답
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Bucket: user

{ "error": "rate_limited", "message": "Too many requests. Please try again in a moment.", "retry_after_sec": 37 }
  1. Retry-After 또는 retry_after_sec를 읽습니다
  2. 최소 그만큼의 초를 기다리고, 모든 프로세스가 같은 순간에 다시 시작하지 않도록 약간의 무작위 시간을 더합니다
  3. 요청은 최대 몇 번만 다시 시도합니다
  4. 계속 429를 받으면 작업을 더 긴 시간에 걸쳐 나눕니다
Node.js: 토큰 캐시와 429 처리
const BASE = 'https://dock.at-harbor.com';
let cached = null; // { token, expiresAt }

async function getToken() {
  if (cached && Date.now() < cached.expiresAt - 60_000) return cached.token;
  const res = await fetch(BASE + '/v1/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      key_id: process.env.LH_KEY_ID,
      key_secret: process.env.LH_KEY_SECRET,
    }),
  });
  if (!res.ok) throw new Error('Token exchange failed: HTTP ' + res.status);
  const data = await res.json();
  cached = { token: data.token, expiresAt: Date.now() + data.expires_in * 1000 };
  return cached.token;
}

async function lh(method, path, body, attempt = 0) {
  const headers = { Authorization: 'Bearer ' + (await getToken()) };
  if (body !== undefined) headers['Content-Type'] = 'application/json';

  const res = await fetch(BASE + path, {
    method,
    headers,
    body: body === undefined ? undefined : JSON.stringify(body),
  });

  if (res.status === 429 && attempt < 3) {
    const wait = Number(res.headers.get('Retry-After') ?? 5);
    await new Promise((r) => setTimeout(r, (wait + Math.random() * 2) * 1000));
    return lh(method, path, body, attempt + 1);
  }
  if (res.status === 401 && attempt === 0) {
    cached = null; // 토큰 만료 – 새 토큰으로 한 번 더 시도
    return lh(method, path, body, attempt + 1);
  }
  const data = await res.json().catch(() => null);
  if (!res.ok) throw new Error((data && data.error) + ' (HTTP ' + res.status + ')');
  return data;
}

// 예: const { items } = await lh('GET', '/v1/pages');

대량 데이터를 위한 팁

  • 토큰을 캐시하고 15분 만료 직전에만 갱신
  • X-RateLimit-Remaining을 보고 적어지면 속도 줄이기
  • 수십 개의 병렬 연결 대신 요청을 차례로 보내기
  • 페이지마다 따로 조회하지 말고 목록을 한 번 불러와 로컬에서 거르기
  • 큰 동기화는 몇 분에 걸쳐 나누기
  • 5xx 오류는 간격을 늘려 가며 다시 시도하되 4xx 오류는 다시 시도하지 않기

기타 한도

대상한도응답
로그인 실패이메일 주소와 IP별로 5, 10, 20번 실패하면 잠금: 60초, 5분, 30분429 account_locked
폼 제출모든 폼을 합쳐 IP 주소당 시간당 10건429 form_rate_limited

토큰 교환에 실패하면 키와 IP가 일시적으로 잠깁니다. API 키와 토큰을 참고하세요. 푸시 알림은 대시보드에서만 보내며, 그 한도는 팔로워와 푸시 알림에서 설명합니다.

페이지, 링크, 블록, 단축 링크 같은 플랜의 콘텐츠 한도는 요청 한도가 아니라 고정된 상한입니다. 플랜 비교에 정리되어 있습니다.

요청 크기

API는 본문을 읽기 전에 크기를 잽니다. 허용되는 크기는 엔드포인트에 따라 다릅니다.

엔드포인트최대 본문
로그인, 가입 등 /v1/auth 아래의 인증 없는 호출, POST /v1/token, POST /v1/oauth/exchange, /oauth 아래의 AI 어시스턴트용 OAuth 엔드포인트와 그 동의, POST /v1/ingest/{id}, 후원하기 블록 결제, 앱의 구독 호출(팔로우, 컬렉션, 푸시 기기), 워크스페이스 전환, POST /v1/auth/logout-all, 계정 삭제, 2단계 인증 설정, 초대 수락, 추천 프로그램 활성화, 연동 연결 시의 복귀 단계64 KB
페이지에서 폼 제출을 포함한 그 밖의 모든 JSON 엔드포인트1 MB
POST /v1/webhooks/{id}/verify-signature와 /mcp의 MCP 서버2 MB
파일 업로드(POST /v1/assets와 POST /v1/pages/{id}/uploads, multipart/form-data)요청 전체 11 MB, 파일 자체는 최대 10 MB

본문이 더 크면 API가 413 request_too_large로 응답하며, details.max_bytes와 details.max_kb에 한도가 나옵니다. 업로드에서 파일이 너무 크면 413 payload_too_large가 됩니다. DELETE 호출처럼 본문이 없는 엔드포인트는 본문을 받지 않거나 {}만 받습니다.

413 응답
HTTP/1.1 413 Payload Too Large

{ "error": "request_too_large", "message": "…", "details": { "max_bytes": 65536, "max_kb": 64 }, "request_id": "…" }

본문 안에도 한도가 있습니다. 모든 텍스트에는 최대 길이가, 모든 목록에는 최대 항목 수가 있으며, 알 수 없는 필드는 거부됩니다. 값은 API 레퍼런스에 엔드포인트별로 나와 있습니다. 위반하면 422 validation_failed가 되며, details에 필드, 규칙, 한도가 나옵니다. 요청의 경로와 쿼리 문자열은 합쳐서 최대 16 KB입니다.

원하는 내용을 찾지 못했나요?

필요한 내용을 알려 주시면 지원팀이 직접 도와드립니다.

지원팀에 문의