Skip to content
Language: English

API for developers

Rate limits: every limit and header

Updated 1 November 2026 5 min read

How many requests per minute the LinkHarbor API allows, how large a request may be, how to read the headers and how to handle a 429 properly.

To keep the API fast for everyone, LinkHarbor limits the number of requests per minute. The limits do not depend on your plan – there is no monthly quota of API calls. This article lists every limit and shows how your code can handle them cleanly.

How requests are counted

  • Every request falls into exactly one bucket, for example user for authenticated requests or token for the token exchange
  • Counting is per full minute (UTC); the counter starts at zero again with every new minute
  • Depending on the bucket, the API counts per workspace, per person, per connection or per IP address
  • Requests that already fail on the token, scope or schema (401, 403 insufficient_scope, 400, 422 validation_failed from the schema check) do not count; later errors such as 403 feature_not_available do

All limits

Bucket (X-RateLimit-Bucket)LimitCounted perApplies to
user600 per minuteWorkspace for tokens from API keys (all keys and AI tools of the workspace together); person for dashboard sign-insAll authenticated requests to /v1 that have no bucket of their own
token30 per minuteIP addressPOST /v1/token
public120 per minuteIP addressRequests to the API without authentication, such as GET /v1/plans and GET /v1/pages/available
callback120 per minuteIP addressPOST /v1/callbacks/sale
internal120 per minuteIP addressIngest URL of the “Custom (API)” integration (POST /v1/ingest/{id})
mcp120 per minuteAI connection (access token)MCP server at /mcp
oauth300 per minuteIP addressOAuth sign-in and discovery for AI assistants
auth30 per minuteIP addressLogin, registration, password reset (dashboard and app)
refresh120 per minuteIP addressExtending a dashboard session
discover60 per minutePerson, IP address when not signed inThe app's Discover section
subs_read120 per minutePerson, IP address when not signed inReading the pages you follow (app)
subs_write20 per minutePerson, IP address when not signed inChanging the pages you follow (app)

One budget per workspace

All tokens from API keys in the same workspace share the 600 requests per minute – no matter how many keys you create. The tools of connected AI assistants count here too, because they call the API internally. So more keys do not raise the limit. If a lot of load is spread across several programs, plan around the shared budget.

The headers

X-RateLimit-Limit
The bucket's limit per minute, for example 600.
X-RateLimit-Remaining
How many requests are left in the current minute.
X-RateLimit-Bucket
The bucket the request fell into, for example user.
Retry-After
Only with 429 rate_limited: seconds until the next minute starts. With api_key_locked, the waiting time is in details.retry_after_sec.

When the limit is reached

Once the limit is reached, the API responds with HTTP 429 and this body. The text in message is in the account language (see API error format), here in English. Rely on the code rate_limited:

Response for 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. Read Retry-After or retry_after_sec
  2. Wait at least that many seconds, plus a little randomness so that not all processes start again at the same moment
  3. Retry the request a few times at most
  4. If you keep getting 429, spread the work over more time
Node.js: cache the token and handle 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; // token expired – try once more with a new token
    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;
}

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

Tips for large amounts of data

  • Cache the token and only renew it shortly before it expires after 15 minutes
  • Watch X-RateLimit-Remaining and slow down when it gets low
  • Send requests one after another instead of over dozens of parallel connections
  • Load lists once and filter them locally instead of querying each page individually
  • Spread large syncs over several minutes
  • Retry 5xx errors with increasing pauses, but not 4xx errors

Other limits

WhatLimitResponse
Failed loginsLockout after 5, 10 and 20 failed attempts per e-mail address and IP: 60 seconds, 5 minutes, 30 minutes429 account_locked
Form submissions10 per hour and IP address, across all forms429 form_rate_limited

Failed attempts at the token exchange temporarily lock the key and IP, see API keys and tokens. You only send push notifications in the dashboard; their limits are explained in Followers and push notifications.

Your plan's content limits – pages, links, blocks, short links and so on – are not rate limits but fixed caps. They are listed in Plans compared.

Request size

Every body is measured before the API reads it. How large it may be depends on the endpoint:

EndpointsLargest body
Login, registration and the other unauthenticated calls under /v1/auth, POST /v1/token, POST /v1/oauth/exchange, the OAuth endpoints for AI assistants under /oauth and the consent for them, POST /v1/ingest/{id}, the Support block checkout, the app's subscription calls (following, collections, push devices), switching workspaces, POST /v1/auth/logout-all, deleting an account, two-factor setup, accepting an invitation, activating the referral programme and the return step when connecting an integration64 KB
All other JSON endpoints, including submitting a form on a page1 MB
POST /v1/webhooks/{id}/verify-signature and the MCP server at /mcp2 MB
File uploads (POST /v1/assets and POST /v1/pages/{id}/uploads, multipart/form-data)11 MB for the whole request, the file itself at most 10 MB

If the body is larger, the API responds with 413 request_too_large; details.max_bytes and details.max_kb give the limit. A file that is too large in an upload leads to 413 payload_too_large. Endpoints without a body, such as the DELETE calls, accept no body or only {}.

Response for 413
HTTP/1.1 413 Payload Too Large

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

There are limits inside the body too: every text has a maximum length, every list a maximum number of entries, and unknown fields are rejected. The values are listed per endpoint in the API reference. A violation leads to 422 validation_failed, and details names the field, rule and limit. The path and query string of a request may be at most 16 KB long together.

Nothing found?

Tell us what you need, and our support team will help you personally.

Contact support