LinkHarbor APIで1分あたりに許可されるリクエスト数、リクエストの最大サイズ、ヘッダーの読み方、429の適切な扱い方を説明します。
すべての人にとってAPIを高速に保つため、LinkHarborは1分あたりのリクエスト数を制限しています。上限はプランに依存せず、API呼び出しの月間の枠もありません。この記事では、すべての上限を示し、コードでそれらをきれいに扱う方法を説明します。
リクエストの数え方
- 各リクエストはちょうど1つのバケットに入ります(例:認証済みリクエストはuser、トークンの交換はtoken)
- カウントは1分(UTC)単位で、新しい1分ごとにカウンターはゼロから始まります
- バケットによって、APIはワークスペースごと、人ごと、接続ごと、IPアドレスごとに数えます
- トークン、スコープ、スキーマの段階ですでに失敗するリクエスト(401、403 insufficient_scope、400、スキーマチェックによる422 validation_failed)は数えられません。403 feature_not_availableなど、その後のエラーは数えられます
すべての上限
| バケット(X-RateLimit-Bucket) | 上限 | カウント単位 | 対象 |
|---|---|---|---|
| user | 1分あたり600 | APIキーのトークンではワークスペース(ワークスペースのすべてのキーとAIツールの合計)、ダッシュボードのログインでは人 | 専用のバケットを持たない/v1へのすべての認証済みリクエスト |
| token | 1分あたり30 | IPアドレス | POST /v1/token |
| public | 1分あたり120 | IPアドレス | GET /v1/plansやGET /v1/pages/availableなど、認証なしのAPIへのリクエスト |
| callback | 1分あたり120 | IPアドレス | POST /v1/callbacks/sale |
| internal | 1分あたり120 | IPアドレス | 「Custom (API)」連携のIngest URL(POST /v1/ingest/{id}) |
| mcp | 1分あたり120 | AI接続(アクセストークン) | /mcpのMCPサーバー |
| oauth | 1分あたり300 | IPアドレス | AIアシスタント向けのOAuthログインとディスカバリー |
| auth | 1分あたり30 | IPアドレス | ログイン、登録、パスワードのリセット(ダッシュボードとアプリ) |
| refresh | 1分あたり120 | IPアドレス | ダッシュボードのセッションの延長 |
| discover | 1分あたり60 | 人、ログインしていない場合はIPアドレス | アプリの「見つける」セクション |
| subs_read | 1分あたり120 | 人、ログインしていない場合はIPアドレス | フォロー中のページの読み取り(アプリ) |
| subs_write | 1分あたり20 | 人、ログインしていない場合はIPアドレス | フォロー中のページの変更(アプリ) |
ワークスペースごとに1つの枠
同じワークスペースのAPIキーのトークンはすべて、キーをいくつ作成しても、1分あたり600件のリクエストを共有します。接続したAIアシスタントのツールも内部でAPIを呼び出すため、ここに含まれます。そのため、キーを増やしても上限は上がりません。複数のプログラムに大きな負荷が分散する場合は、共有の枠を前提に計画してください。
ヘッダー
- X-RateLimit-Limit
- バケットの1分あたりの上限です(例: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を頼りにしてください。
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 } - Retry-Afterまたはretry_after_secを読み取ります
- すべてのプロセスが同時に再開しないよう、少なくともその秒数に少しランダムな時間を加えて待ちます
- リクエストの再試行は数回までにします
- 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を監視し、残りが少なくなったら速度を落とします
- 数十の並列接続ではなく、リクエストを1つずつ順番に送ります
- 各ページを個別に問い合わせるのではなく、リストを一度読み込んでローカルで絞り込みます
- 大きな同期は数分に分散させます
- 5xxエラーは間隔を広げながら再試行しますが、4xxエラーは再試行しません
その他の上限
| 対象 | 上限 | レスポンス |
|---|---|---|
| ログインの失敗 | メールアドレスとIPごとに5回、10回、20回の失敗でロック:60秒、5分、30分 | 429 account_locked |
| フォームの送信 | すべてのフォームを合わせて、IPアドレスごとに1時間あたり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、アカウントの削除、二要素認証の設定、招待の承認、紹介プログラムの有効化、連携の接続時の戻りのステップ | 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の呼び出しなど本文のないエンドポイントは、本文なしか{}だけを受け付けます。
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までです。
こちらもおすすめ
答えが見つかりませんか?
お困りのことをお知らせください。サポートチームが個別にお手伝いします。