API RU
Early access. API keys are issued on request. How to get a key

Rate limits are counted in weight units, so heavier requests cost more. Going over the limit returns 429. Overload or temporary unavailability returns 503. Both responses tell you when to retry.

#Request weight

EndpointWeight
GET /v1/me1
GET /v1/workspaces1
GET /v1/campaigns2
GET /v1/campaigns/{id}1
GET /v1/leads2
GET /v1/leads/{id}1
GET /v1/leads/{id}/messages3
GET /v1/events2, 3 with include_text=true

#Limits by plan

Limits apply both to each key and to the whole organization, across all of its keys combined.

WindowProPremiumEnterprise
Per key, per second51010
Per key, per minute300600600
Per organization, per second102525
Per organization, per minute60015001500
Concurrent requests per key555
Concurrent requests per organization101010

For example, on the Pro plan one key can make 2 lead list requests per second (weight 2, limit 5) and up to 150 such requests per minute. GET /v1/me returns the key's current limits in the limits field.

The API also limits requests from a single IP address: 20 per second on average, with short bursts up to 40. If one address receives 20 responses with status 401 within 5 minutes, the address is blocked for 15 minutes, and all its requests get 429 with Retry-After. Check your key before you retry requests that failed with 401.

Sometimes limits are temporarily reduced, for example during maintenance on our side. When that happens, limits.mode returns emergency and the limit fields show lower values. Code that reads the response headers adjusts on its own.

#Headers

Successful responses and 429 responses for key and organization limits include the limit state. A 429 response for the IP address limit doesn't include these headers, only Retry-After.

HTTP
RateLimit-Policy: "key_second";q=5;w=1, "key_minute";q=300;w=60, "org_second";q=10;w=1, "org_minute";q=600;w=60
RateLimit: "key_second";r=3;t=1, "key_minute";r=291;t=60, "org_second";r=8;t=1, "org_minute";r=583;t=60
RateLimit-Limit: 5
RateLimit-Remaining: 3
RateLimit-Reset: 1
HeaderValue
RateLimit-PolicyThe windows: name, limit q in weight units and window length w in seconds.
RateLimitFor each window, the remaining amount r and the seconds until reset t.
RateLimit-LimitThe limit of the window closest to running out.
RateLimit-RemainingThe amount remaining in that window.
RateLimit-ResetSeconds until that window resets.

#429 response

If a request's weight exceeds the remaining amount, or you exceed the number of concurrent requests, you get 429 rate_limit_exceeded with a Retry-After header in seconds. The request was not processed. Wait Retry-After seconds and add random jitter so that several processes don't retry at the same moment.

#503 response

503 means the request wasn't processed right now. The code tells you why:

CodeReasonRetry-After
server_busyThe service is overloaded1 second
service_unavailableThe data couldn't be read for now5 seconds
integration_initializingA new key is still being prepared5 seconds

Retry with a growing delay: start with Retry-After, then double it up to 60 seconds. If you keep getting 503 for more than a few minutes, contact support and include the Request-Id.

#Retries are safe

All v1 routes only read data, so you can retry any request without side effects. An error doesn't advance the cursor: retrying with the same cursor returns the same page.

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function meliora(path, params = {}, maxAttempts = 6) {
  const url = new URL(`https://api.melioralab.io/v1${path}`);
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
  for (let attempt = 1; ; attempt++) {
    let res;
    try {
      res = await fetch(url, {
        headers: { Authorization: `Bearer ${process.env.MELIORA_API_KEY}` },
        signal: AbortSignal.timeout(15_000),
      });
    } catch (networkError) {
      if (attempt >= maxAttempts) throw networkError;
      await sleep(Math.min(60, 2 ** attempt) * 1000);
      continue;
    }
    if (res.ok) return res.json();
    const problem = await res.json().catch(() => ({}));
    if ((res.status === 429 || res.status === 503) && attempt < maxAttempts) {
      const base = Number(res.headers.get('Retry-After')) || 5;
      const wait = Math.min(60, base * (res.status === 503 ? 2 ** (attempt - 1) : 1));
      await sleep((wait + Math.random() * Math.max(1, wait * 0.25)) * 1000);
      continue;
    }
    const error = new Error(`Meliora API ${res.status} ${problem.code ?? ''}`);
    Object.assign(error, { status: res.status, code: problem.code, requestId: res.headers.get('Request-Id') });
    throw error;
  }
}

#Staying within limits

  • Poll the event feed once a minute instead of walking all leads on a schedule.
  • Use limit=100 for lists and up to 200 for the feed: fewer requests for the same amount of data.
  • Filter on our side: campaign_id, status and username cost less than exporting the whole list.
  • Don't run more than 5 concurrent requests per key. Extra requests get 429.
  • Request message text only where you need it: events with include_text=true and conversations have a weight of 3.